stringr

stringr no es parte del tidyverse, por lo que debemos instalarlo y cargarlo explícitamente.

library(tidyverse)
library(stringr)

Conceptos básicos

Podemos crear cadenas con comillas simples o comillas dobles. A diferencia de otros lenguajes, no hay diferencia en el comportamiento. Es recomendable usar siempre ", a menos que queramos crear una cadena que contenga múltiples comillas dobles.

string1 <- "Esto es una cadena"
string2 <- 'Si queremos incluir una "cita" dentro de una cadena, usamos comillas simples'

Si olvidamos cerrar una cadena, veremos +, el caracter de continuación:

# > "Esta es una cadena sin comillas de cierre
# + 
# + 
# + AYUDA, ESTOY ATORADO!

Si esto nos pasa, sólo debemos presionar Escape e intentar nuevamente.

Para incluir una comilla simple o doble literal en una cadena, podemos usar \ para “escaparla”:

double_quote <- "\"" # o '"'
single_quote <- '\'' # o "'"

Esto significa que si deseamos incluir una barra invertida, tendremos que duplicarla: “\”.

Debemos tener en cuenta que la representación impresa de una cadena no es la misma que la cadena por si misma, esto se debe a que la representación impresa muestra los escapes. Para ver el contenido “crudo” de la cadena, usamos writeLines():

x <- c("\"", "\\")
x
## [1] "\"" "\\"
writeLines(x)
## "
## \

Hay un puñado de otros caracteres especiales. Los más comunes son "\n", nueva línea y "\t", tab, pero podemos ver la lista completa en la ayuda de ": ?'"' o ?"'". En ocasiones nos podemos encontrar con cadenas como "\u00b5" esta es una forma de escribir caracteres no alfabéticos que funcionan en todas las plataformas:

x <- "\u00b5"
x
## [1] "<U+00B5>"

Múltiples cadenas a menudo se almacenan en un vector de caracteres, que podemos crear con c():

c("one", "two", "three")
## [1] "one"   "two"   "three"

Longitud

R base contiene muchas funciones para trabajar con cadenas pero las evitaremos porque pueden ser inconsistentes, lo que las hace difíciles de recordar. En su lugar usaremos funciones de stringr. Estas tienen nombres más intuitivos y todos comienzan con str_. Por ejemplo, str_length() nos devuelve la cantidad de caracteres en una cadena:

str_length(c("a", "Ciencia de Datos con R", NA))
## [1]  1 22 NA

El prefijo común str_ es particularmente útil si utilizamos RStudio, ya que al escribir str_ activará la función autocompletar, lo que nos permite ver todas las funciones del stringr:

Combinando cadenas

Para combinar dos o más cadenas, usamos str_c():

str_c("x", "y")
## [1] "xy"
str_c("x", "y", "z")
## [1] "xyz"

Podemos usar el argumento sep para controlar la forma en que se separan las cadenas de texto:

str_c("x", "y", sep = ", ")
## [1] "x, y"

Como en la mayoría de las otras funciones en R, los valores faltantes son contagiosos. Si deseamos que se impriman como "NA", usamos str_replace_na():

x <- c("abc", NA)
str_c("|-", x, "-|")
## [1] "|-abc-|" NA
str_c("|-", str_replace_na(x), "-|")
## [1] "|-abc-|" "|-NA-|"

Como se muestra arriba, str_c() se vectoriza, y recicla automáticamente vectores más cortos a la misma longitud que el más largo:

str_c("prefix-", c("a", "b", "c"), "-suffix")
## [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"

Los objetos de longitud 0 se eliminan en “silencio”. Esto es particularmente útil en conjunto con if:

name <- "Ana"
time_of_day <- "días"
birthday <- FALSE

str_c(
  "Buenos ", time_of_day, " ", name,
  if (birthday) " y FELIZ CUMPLEAÑOS",
  "."
)
## [1] "Buenos d<U+00ED>as Ana."

Para colapsar un vector de cadenas en una sola cadena, usamos collapse:

str_c(c("x", "y", "z"), collapse = ", ")
## [1] "x, y, z"

Subconjuntos de cadenas

Podemos extraer partes de una cadena usando str_sub(). Además de la cadena, str_sub() necesita los argumentos start y end que dan la posición (inclusiva) de la subcadena:

x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
## [1] "App" "Ban" "Pea"
str_sub(x, -3, -1)
## [1] "ple" "ana" "ear"

Debemos notar que str_sub() no fallará si la cadena es demasiado corta, solo regresará tanto como sea posible:

str_sub("a", 1, 5)
## [1] "a"

También podemos usar la forma de asignación de str_sub() para modificar cadenas:

str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1))
x
## [1] "apple"  "banana" "pear"

Locales

Cambiar de mayúsculas a minúsculas es más complicado de lo que podría parecer a primera vista, ya que los diferentes idiomas tienen diferentes reglas para cambiar de mayúscula a minúscula. Podemos elegir qué conjunto de reglas usar al especificar una configuración regional:

# El Turco tiene dos i's: con y sin punto y tiene
# una regla diferente para pasarlas a mayúsculas:
str_to_upper(c("i", "??"))
## [1] "I"  "??"
str_to_upper(c("i", "??"), locale = "tr")
## [1] "<U+0130>" "??"

La configuración regional se especifica como un código de idioma ISO 639, que es una abreviación de dos o tres letras. Podemos consultar la lista de Wikipedia para conocer el código de distintos idiomas, incluído el nuestro. Si dejamos la configuración regional en blanco, usará la configuración regional actual, tal como lo proporciona nuestro sistema operativo.

Otra operación importante que se ve afectada por la configuración regional es el ordenamiento. Las funciones de R base order() y sort() ordenan cadenas usando la configuración regional actual. Si deseamos un comportamiento robusto en diferentes computadoras, es posible que debamos utilizar str_sort() y str_order() que toman un argumento adicional locale:

x <- c("apple", "eggplant", "banana")
str_sort(x, locale = "en")  # Inglés
## [1] "apple"    "banana"   "eggplant"
str_sort(x, locale = "haw") # Hawaiano
## [1] "apple"    "eggplant" "banana"

Patrones con expresiones regulares

Regexps es un lenguaje muy conciso que nos permite describir patrones en cadenas.

Para aprender expresiones regulares, usaremos str_view() y str_view_all(). Estas funciones toman un vector de caracteres y una expresión regular y muestran cómo coinciden. Comenzaremos con expresiones regulares muy simples y luego gradualmente lo complicaremos. Una vez que dominemos la coincidencia de patrones, aprenderemos cómo aplicar esas ideas con varias funciones de cadenas.

Patrones básicos

Los patrones más simples coinciden con cadenas exactas:

x <- c("apple", "banana", "pear")
str_view(x, "an")

El siguiente paso en la complejidad es ., que coincide con cualquier caracter (excepto una nueva línea):

str_view(x, ".a.")

Pero si “.” coincide con cualquier caracter, ¿cómo encontramos el caracter “.”? Debemos usar un “escape” para decirle a la expresión regular que deseamos coincidirlo exactamente en vez de usar su comportamiento especial. Al igual que las cadenas, las expresiones regulares usan la barra invertida, \, para escapar del comportamiento especial. Entonces, para que coincida con un ., necesitamos la expresión regular \.. Lamentablemente, esto crea un problema. Usamos cadenas para representar expresiones regulares, y \ también se usa como un símbolo de escape en cadenas. Entonces para crear la expresión regular \. necesitamos la cadena "\\.".

# Para crear la expresión regular, necesitamos \\
dot <- "\\."

# Pero la expresión sólo contiene un backslash:
writeLines(dot)
## \.
# Con esto, le decimos a R que busque un . explícito
str_view(c("abc", "a.c", "bef"), "a\\.c")

Si \ se usa como un carácter de escape en expresiones regulares, ¿cómo se combina un literal \? Necesitamos escapar, creando la expresión regular \\. Para crear esa expresión regular, necesitamos usar una cadena, que también necesita escapar \. Eso significa que para hacer coincidir un literal \ necesitamos escribir "\\\\" ¡necesitamos cuatro barras invertidas para que coincida con una!

x <- "a\\b"
writeLines(x)
## a\b
str_view(x, "\\\\")

A partir de ahora, escribiremos la expresión regular como \. y las cadenas que representan la expresión regular como "\\.".

Anclajes

Por defecto, las expresiones regulares coincidirón con cualquier parte de una cadena. A menudo es útil anclar la expresión regular para que coincida desde el principio o el final de la cadena. Podemos usar:

  • ^ para que coincida con el comienzo de la cadena.

  • $ para que coincida el final de la cadena.

x <- c("apple", "banana", "pear")
str_view(x, "^a")
str_view(x, "a$")

Para recordar cuál es cuál, prueba esta mnemotecnia de Evan Misshula: si comienzas con potencia (^), terminas con dinero ($).

Para forzar una expresión regular a que sólo coincida con una cadena completa, debemos anclarla con ambos ^ y $:

x <- c("apple pie", "apple", "apple cake")
str_view(x, "apple")
str_view(x, "^apple$")

También podemos coincidir el límite entre las palabras con \b.

Clases de caracteres y alternativas

Hay una serie de patrones especiales que coinciden con más de un caracter. Ya vimos ., que coincide con cualquier caracter que no sea una nueva línea. Hay otras cuatro herramientas útiles:

  • \d: coincide con cualquier dígito.

  • \s: coincide con cualquier espacio en blanco.

  • [abc]: coincide con a, b, o c.

  • [^abc]: coincide con cualquier cosa excepto a, b, o c.

Recordemos que para crear una expresión regular que contenga \d o \s, debemos escapar \ de la cadena, por lo que escribiremos "\\d" o "\\s".

Una clase de caracteres que contenga un solo carácter es una buena alternativa a los escapes de barra invertida cuando se desea incluir un metacarácter único en una expresión regular. Muchas personas lo encuentran más legible.

# Buscar un caracter que normalmente tiene un significado especial en regex
str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")
str_view(c("abc", "a.c", "a*c", "a c"), "a[ ]")

Esto funciona para la mayoría (pero no todos) de los metacaracteres de expresiones regulares: $ . | ? * + ( ) [ {. Desafortunadamente, algunos caracteres tienen un significado especial incluso dentro de una clase de caracteres y deben manejarse con escapes de barra invertida: ] \ ^ y -.

Podemos usar la alternancia para elegir uno o más patrones alternativos. Por ejemplo, abc|d..f coincidirá con "abc" o "deaf". Notemos que la precedencia para | es baja, por lo que abc|xyz coincide abc o xyz no abcyz o abxyz. Al igual que con las expresiones matemáticas, si la precedencia se vuelve confusa, usamos paréntesis para dejar en claro lo que deseamos:

str_view(c("grey", "gray"), "gr(e|a)y")

Repetición

El siguiente paso en complejidad implica controlar cuántas veces coincide un patrón:

  • ?: 0 o 1

  • +: 1 o más

  • *: 0 o más

x <- "1888 es el año más largo en números romanos: MDCCCLXXXVIII"
str_view(x, "CC?")
str_view(x, "CC+")
str_view(x, 'C[LX]+')

Notemos que la precedencia de estos operadores es alta, por lo que podemos escribir: colou?r para que coincida con la ortografía estadounidense o británica, por ejemplo. Eso significa que la mayoría de los usos necesitarán paréntesis, como bana(na)+.

También podemos especificar el número de coincidencias con precisión:

  • {n}: exactamente n
  • {n,}: n o más
  • {,m}: a lo sumo m
  • {n,m}: entre n y m
str_view(x, "C{2}")
str_view(x, "C{2,}")
str_view(x, "C{2,3}")

Por defecto, estas coincidencias son “codiciosas”: coincidirán con la cadena más larga posible. Podemos hacerlas “flojas”, haciendo coincidir la cadena más corta posible poniendo un ? después de ellas. Esta es una característica avanzada de las expresiones regulares, pero es útil saber que existe:

str_view(x, 'C{2,3}?')
str_view(x, 'C[LX]+?')

Agrupación y referencias

Anteriormente, aprendimos que los paréntesis son una forma de desambiguar expresiones complejas. Los paréntesis también crean un grupo de captura numerado (número 1, 2, etc.). Un grupo de captura almacena la parte de la cadena que coincide con la parte de la expresión regular dentro de los paréntesis. Podemos consultar textos anteriores con un grupo de captura con referencias, como \1, \2, etc. Por ejemplo, la siguiente expresión regular encuentra todas las frutas que tienen un par de letras repetidas.

str_view(fruit, "(..)\\1", match = TRUE)

(Pronto, veremos cómo son útiles junto con str_match().)

Herramientas

Ahora que conocemos los conceptos básicos de las expresiones regulares, es hora de aprender cómo aplicarlas a problemas reales. Aprenderemos funciones de stringr que nos permiten:

  • Determinar qué cadenas coinciden con un patrón.
  • Encontrar las posiciones de las coincidencias.
  • Extraer el contenido de las coincidencias.
  • Reemplazar coincidencias con nuevos valores.
  • Dividir una cadena basado en una coincidencia.

Detectar coincidencias

Para determinar si un vector de caracteres coincide con un patrón, usamos str_detect(). Devuelve un vector lógico con la misma longitud que la entrada:

x <- c("apple", "banana", "pear")
str_detect(x, "e")
## [1]  TRUE FALSE  TRUE

Recordemos que cuando se utiliza un vector lógico en un contexto numérico, FALSE se convierte en 0 y TRUE se convierte en 1. Esto hace que sum() y mean() sean utiles si deseamos responder preguntas sobre coincidencias en un vector más grande:

# ¿Cuántas palabras empiezan con t?
sum(str_detect(words, "^t"))
## [1] 65
# ¿Qué proporción de palabras terminan en vocal?
mean(str_detect(words, "[aeiou]$"))
## [1] 0.2765306

Cuando tenemos condiciones lógicas complejas (por ejemplo, coincidir con a o b, pero no con c a menos que d) a menudo es más fácil combinar múltiples llamadas de str_detect() con operadores lógicos, en lugar de tratar de crear una única expresión regular. Por ejemplo, aquí hay dos formas de encontrar todas las palabras que no contienen vocales:

# Encuentra todas las palabras que contengan al menos una vocal, y niegalas
no_vowels_1 <- !str_detect(words, "[aeiou]")

# Encuentra todas las palabras que están compuestas por puras consonantes
no_vowels_2 <- str_detect(words, "^[^aeiou]+$")
identical(no_vowels_1, no_vowels_2)
## [1] TRUE

Los resultados son idénticos, pero el primer enfoque es significativamente más fácil de entender. Si una expresión regular se vuelve demasiado complicada, debemos intentar dividirla en partes más pequeñas, dándole un nombre a cada pieza y luego combinando las piezas con operaciones lógicas.

Un uso común de str_detect() es seleccionar los elementos que coinciden con un patrón. Podemos hacer esto con un subconjunto lógico o el str_subset() conveniente :

words[str_detect(words, "x$")]
## [1] "box" "sex" "six" "tax"
str_subset(words, "x$")
## [1] "box" "sex" "six" "tax"

Normalmente, sin embargo, las cadenas serán una columna de un data frame, y en su lugar preferiremos usar filter:

df <- tibble(
  word = words, 
  i = seq_along(word)
)
df %>% 
  filter(str_detect(words, "x$"))

Una variación de str_detect() es str_count(): en lugar de un simple sí o no, nos dice cuántas coincidencias hay en una cadena:

x <- c("apple", "banana", "pear")
str_count(x, "a")
## [1] 1 3 1
# En promedio, ¿cuántas vocales por palabra?
mean(str_count(words, "[aeiou]"))
## [1] 1.991837

Es natural usar str_count() con mutate():

df %>% 
  mutate(
    vowels = str_count(word, "[aeiou]"),
    consonants = str_count(word, "[^aeiou]")
  )

Debemos tener en cuenta que las coincidencias nunca se superponen. Por ejemplo, en "abababa", ¿cuántas veces coincidirá el patrón "aba"? Las expresiones regulares dicen dos, no tres:

str_count("abababa", "aba")
## [1] 2
str_view_all("abababa", "aba")

Muchas funciones de stringr vienen en pares: una función funciona con una sola coincidencia, y la otra funciona con todas las coincidencias. La segunda función tendrá el sufijo _all.

Extraer coincidencias

Para extraer el texto real de una coincidencia, usamos str_extract(). Para demostrarlo, vamos a necesitar un ejemplo más complicado. Usaremos las Oraciones de Harvard, que fueron diseñadas para probar sistemas VOIP, pero también son útiles para practicar expresiones regulares. Estas se encuentran en stringr::sentences:

length(sentences)
## [1] 720
head(sentences)
## [1] "The birch canoe slid on the smooth planks." 
## [2] "Glue the sheet to the dark blue background."
## [3] "It's easy to tell the depth of a well."     
## [4] "These days a chicken leg is a rare dish."   
## [5] "Rice is often served in round bowls."       
## [6] "The juice of lemons makes fine punch."

Imagina que queremos encontrar todas las oraciones que contienen un color. Primero creamos un vector de nombres de colores y luego lo convertimos en una única expresión regular:

colours <- c("red", "orange", "yellow", "green", "blue", "purple")
colour_match <- str_c(colours, collapse = "|")
colour_match
## [1] "red|orange|yellow|green|blue|purple"

Ahora podemos seleccionar las oraciones que contienen un color, y luego extraer el color para descubrir cuál es:

has_colour <- str_subset(sentences, colour_match)
matches <- str_extract(has_colour, colour_match)
head(matches)
## [1] "blue" "blue" "red"  "red"  "red"  "blue"

Notemos que str_extract() solo extrae la primera coincidencia. Podemos ver eso más fácilmente al seleccionar primero todas las oraciones que tienen más de 1 coincidencia:

more <- sentences[str_count(sentences, colour_match) > 1]
str_view_all(more, colour_match)
str_extract(more, colour_match)
## [1] "blue"   "green"  "orange"

Este es un patrón común para las funciones de cadenas, porque trabajar con una sola coincidencia nos permite utilizar estructuras de datos mucho más simples. Para obtener todas las coincidencias, usa str_extract_all(). Devuelve una lista:

str_extract_all(more, colour_match)
## [[1]]
## [1] "blue" "red" 
## 
## [[2]]
## [1] "green" "red"  
## 
## [[3]]
## [1] "orange" "red"

Si usamos simplify = TRUE, str_extract_all() devolverá una matriz con coincidencias cortas expandidas a la misma longitud que la más larga:

str_extract_all(more, colour_match, simplify = TRUE)
##      [,1]     [,2] 
## [1,] "blue"   "red"
## [2,] "green"  "red"
## [3,] "orange" "red"
x <- c("a", "a b", "a b c")
str_extract_all(x, "[a-z]", simplify = TRUE)
##      [,1] [,2] [,3]
## [1,] "a"  ""   ""  
## [2,] "a"  "b"  ""  
## [3,] "a"  "b"  "c"
Ejercicios
  1. En el ejemplo anterior, es posible que hayas notado que la expresión regular coincide con “flickered”, que no es un color. Modifica la expresión regular para solucionar el problema.

  2. De los datos de las oraciones de Harvard, extrae:

    • La primera palabra de cada oración
    • Todas las palabras que terminan en ing.
    • Todos los plurales

Coincidencias agrupadas

Ya sabemos que podemos hacer uso de el paréntesis para aclarar la precedencia y para referencias al momento de la comparación. También podemos usarlo para extraer partes de una coincidencia compleja. Por ejemplo, imagina que queremos extraer sustantivos de las oraciones. Como heurística, buscaremos cualquier palabra que venga después de "a" o "the". Definir una “palabra” en una expresión regular es un poco complicado, así que utilizaremos una aproximación simple: una secuencia de al menos un carácter que no es un espacio.

noun <- "(a|the) ([^ ]+)"

has_noun <- sentences %>%
  str_subset(noun) %>%
  head(10)
has_noun %>% 
  str_extract(noun)
##  [1] "the smooth" "the sheet"  "the depth"  "a chicken"  "the parked"
##  [6] "the sun"    "the huge"   "the ball"   "the woman"  "a helps"

str_extract() nos da la coincidencia completa, str_match() nos da a cada componente individual. En lugar de un vector de caracteres, devuelve una matriz, con una columna para la coincidencia completa seguida de una columna para cada grupo:

has_noun %>% 
  str_match(noun)
##       [,1]         [,2]  [,3]     
##  [1,] "the smooth" "the" "smooth" 
##  [2,] "the sheet"  "the" "sheet"  
##  [3,] "the depth"  "the" "depth"  
##  [4,] "a chicken"  "a"   "chicken"
##  [5,] "the parked" "the" "parked" 
##  [6,] "the sun"    "the" "sun"    
##  [7,] "the huge"   "the" "huge"   
##  [8,] "the ball"   "the" "ball"   
##  [9,] "the woman"  "the" "woman"  
## [10,] "a helps"    "a"   "helps"

(Como era de esperar, nuestra heurística para detectar sustantivos es pobre, y también capta adjetivos como smooth y parked).

Si tenemos los datos en un tibble, a menudo es más fácil de usar tidyr::extract(). Funciona de la misma manera str_match() pero requiere que nombremos las coincidencias, que luego se colocan en columnas nuevas:

tibble(sentence = sentences) %>% 
  tidyr::extract(
    sentence, c("article", "noun"), "(a|the) ([^ ]+)", 
    remove = FALSE
  )

Como en str_extract(), si queremos todas las coincidencias para cada cadena, necesitaremos str_match_all().

Reemplazar coincidencias

str_replace() y str_replace_all() nos permiten reemplazar coincidencias con nuevas cadenas. El uso más simple es reemplazar un patrón con una cadena fija:

x <- c("apple", "pear", "banana")
str_replace(x, "[aeiou]", "-")
## [1] "-pple"  "p-ar"   "b-nana"
str_replace_all(x, "[aeiou]", "-")
## [1] "-ppl-"  "p--r"   "b-n-n-"

Con str_replace_all() podemos realizar reemplazos múltiples mediante el suministro de un vector nombrado:

x <- c("1 house", "2 cars", "3 people")
str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))
## [1] "one house"    "two cars"     "three people"

En lugar de reemplazar con una cadena fija, podemos usar referencias para insertar componentes de la coincidencia. Cambiemos la segunda por la tercera palabra:

sentences %>% 
  str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>% 
  head(5)
## [1] "The canoe birch slid on the smooth planks." 
## [2] "Glue sheet the to the dark blue background."
## [3] "It's to easy tell the depth of a well."     
## [4] "These a days chicken leg is a rare dish."   
## [5] "Rice often is served in round bowls."

División

Usamos str_split() para dividir una cadena en pedazos. Por ejemplo, podríamos dividir oraciones en palabras:

sentences %>%
  head(5) %>% 
  str_split(" ")
## [[1]]
## [1] "The"     "birch"   "canoe"   "slid"    "on"      "the"     "smooth" 
## [8] "planks."
## 
## [[2]]
## [1] "Glue"        "the"         "sheet"       "to"          "the"        
## [6] "dark"        "blue"        "background."
## 
## [[3]]
## [1] "It's"  "easy"  "to"    "tell"  "the"   "depth" "of"    "a"     "well."
## 
## [[4]]
## [1] "These"   "days"    "a"       "chicken" "leg"     "is"      "a"      
## [8] "rare"    "dish."  
## 
## [[5]]
## [1] "Rice"   "is"     "often"  "served" "in"     "round"  "bowls."

Debido a que cada componente puede contener un número diferente de elementos, esto devuelve una lista. Si estamos trabajando con un vector de longitud 1, lo más fácil es extraer el primer elemento de la lista:

"a|b|c|d" %>% 
  str_split("\\|") %>% 
  .[[1]]
## [1] "a" "b" "c" "d"

De lo contrario, al igual que las otras funciones de cadena que devuelven una lista, podemos usar simplify = TRUE para obtener una matriz:

sentences %>%
  head(5) %>% 
  str_split(" ", simplify = TRUE)
##      [,1]    [,2]    [,3]    [,4]      [,5]  [,6]    [,7]    
## [1,] "The"   "birch" "canoe" "slid"    "on"  "the"   "smooth"
## [2,] "Glue"  "the"   "sheet" "to"      "the" "dark"  "blue"  
## [3,] "It's"  "easy"  "to"    "tell"    "the" "depth" "of"    
## [4,] "These" "days"  "a"     "chicken" "leg" "is"    "a"     
## [5,] "Rice"  "is"    "often" "served"  "in"  "round" "bowls."
##      [,8]          [,9]   
## [1,] "planks."     ""     
## [2,] "background." ""     
## [3,] "a"           "well."
## [4,] "rare"        "dish."
## [5,] ""            ""

También podemos pedir un número máximo de elementos:

fields <- c("Nombre: Ana", "País: México", "Edad: 29")
fields %>% str_split(": ", n = 2, simplify = TRUE)
##      [,1]          [,2]           
## [1,] "Nombre"      "Ana"          
## [2,] "Pa<U+00ED>s" "M<U+00E9>xico"
## [3,] "Edad"        "29"

En lugar de dividir cadenas por patrones, podemos dividir por carácter, línea, oración y palabra usando boundary():

x <- "Esta es una oración. Esta es otra oración."
str_view_all(x, boundary("word"))
str_split(x, " ")[[1]]
## [1] "Esta"            "es"              "una"             "oraci<U+00F3>n."
## [5] "Esta"            "es"              "otra"            "oraci<U+00F3>n."
str_split(x, boundary("word"))[[1]]
##  [1] "Esta"  "es"    "una"   "oraci" "U"     "00F3"  "n"     "Esta" 
##  [9] "es"    "otra"  "oraci" "U"     "00F3"  "n"

Buscar coincidencias

str_locate() y str_locate_all() nos darán las posiciones inicial y final de cada coincidencia. Estos son particularmente útiles cuando ninguna de las otras funciones hace exactamente lo que deseamos. Podemos usar str_locate() para encontrar el patrón coincidente, str_sub() para extraerlo y/o modificarlo.

Otros tipos de patrones

Cuando utilizamos un patrón que es una cadena, se envuelve automáticamente en una llamada a regex():

# La llamada regular:
str_view(fruit, "nana")
# Es una versión corta de:
str_view(fruit, regex("nana"))

Podemos usar los otros argumentos de regex() para controlar los detalles de la coincidencia:

  • ignore_case = TRUE permite que los caracteres coincidan con sus mayúsculas o minúsculas. Esto siempre usa la configuración regional actual.
bananas <- c("banana", "Banana", "BANANA")
str_view(bananas, "banana")
str_view(bananas, regex("banana", ignore_case = TRUE))
  • multiline = TRUE permite a ^ y $ coincidir el inicio y el final de cada línea en lugar del inicio y el final de la cadena completa.
x <- "Line 1\nLine 2\nLine 3"
str_extract_all(x, "^Line")[[1]]
## [1] "Line"
str_extract_all(x, regex("^Line", multiline = TRUE))[[1]]
## [1] "Line" "Line" "Line"
  • comments = TRUE nos permite usar comentarios y espacios en blanco para hacer que las expresiones regulares complejas sean más comprensibles. Los espacios son ignorados, como lo es todo después #. Para que coincida con un espacio literal, necesitamos escaparlo: "\\ ".
phone <- regex("
  \\(?     # optional opening parens
  (\\d{3}) # area code
  [) -]?   # optional closing parens, space, or dash
  (\\d{3}) # another three numbers
  [ -]?    # optional space or dash
  (\\d{3}) # three more numbers
  ", comments = TRUE)

str_match("514-791-8141", phone)
##      [,1]          [,2]  [,3]  [,4] 
## [1,] "514-791-814" "514" "791" "814"
  • dotall = TRUE permite que . coincida con todo, incluso \n.

Hay otras tres funciones que podemos usar en lugar de regex():

  • fixed(): coincide exactamente con la secuencia de bytes especificada. Ignora todas las expresiones regulares especiales y opera a un nivel muy bajo. Esto nos permite evitar el escape complejo y puede ser mucho más rápido que las expresiones regulares. El siguiente microbenchmark muestra que es aproximadamente 3 veces más rápido en un ejemplo simple.
library(microbenchmark)
microbenchmark::microbenchmark(
  fixed = str_detect(sentences, fixed("the")),
  regex = str_detect(sentences, "the"),
  times = 20
)

Debemos tener cuidado al usar fixed() con datos que no sean en inglés. Es problemático porque a menudo hay múltiples formas de representar al mismo caracter. Por ejemplo, hay dos formas de definir "??": como un solo caracter o como una "a" más un acento:

a1 <- "\u00e1"
a2 <- "a\u0301"
c(a1, a2)
## [1] "<U+00E1>" "a<U+0301>"
a1 == a2
## [1] FALSE

Se representan de forma idéntica, pero debido a que están definidos de manera diferente, fixed() no encuentran una coincidencia. En cambio, podemos usar coll(), definido a continuación, para respetar las reglas de comparación de caracteres humanos:

str_detect(a1, fixed(a2))
## [1] FALSE
str_detect(a1, coll(a2))
## [1] TRUE
  • coll(): compara cadenas usando las reglas de colación estándar. Esto es útil para hacer coincidencias insensibles a mayúsculas y minúsculas. Notemos que coll() toma un parámetro locale que controla qué reglas se usan para comparar caracteres. Desafortunadamente, ¡diferentes partes del mundo usan diferentes reglas!
# Esto significa que debemos estar conscientes de las diferencias cuando hagamos coincidencias con sensibilidad de mayúsculas:
i <- c("I", "??", "i", "??")
i
## [1] "I"  "??" "i"  "??"
str_subset(i, coll("i", ignore_case = TRUE))
## [1] "I" "i"
str_subset(i, coll("i", ignore_case = TRUE, locale = "tr"))
## [1] "i"

Tanto fixed() como regex() tienen argumentos ignore_case, pero no permiten que podamos elegir la configuración regional: siempre utilizan la configuración regional predeterminada. Podemos verla así:

stringi::stri_locale_info()
## $Language
## [1] "es"
## 
## $Country
## [1] "MX"
## 
## $Variant
## [1] ""
## 
## $Name
## [1] "es_MX"

La desventaja de coll() es la velocidad porque las reglas para reconocer qué caracteres son los mismos son complicadas, coll() es relativamente lento en comparación con regex() y fixed().

  • Como vimos con str_split() podemos usar boundary() para unir límites. También podemos usarlo con las otras funciones:
x <- "Esta es una oración."
str_view_all(x, boundary("word"))
str_extract_all(x, boundary("word"))
## [[1]]
## [1] "Esta"  "es"    "una"   "oraci" "U"     "00F3"  "n"

Otros usos de expresiones regulares

Hay dos funciones útiles en R base que también usan expresiones regulares:

  • apropos() busca todos los objetos disponibles del entorno global. Esto es útil si no recordamos el nombre de alguna función.
apropos("replace")
## [1] "%+replace%"       "replace"          "replace_na"      
## [4] "setReplaceMethod" "str_replace"      "str_replace_all" 
## [7] "str_replace_na"   "theme_replace"
  • dir() lista todos los archivos en un directorio. El argumento pattern toma una expresión regular y solo devuelve nombres de archivos que coinciden con el patrón. Por ejemplo, podemos encontrar todos los archivos R Markdown en el directorio actual con:
head(dir(pattern = "\\.Rmd$"))
## [1] "3-AT4A.Rmd"

(Si te sientes más cómodo con “globs” como *.Rmd, puedes convertirlos a expresiones regulares con glob2rx()):

stringi

stringr está construido sobre el paquete stringi. stringr es útil cuando estamos aprendiendo porque expone un conjunto mínimo de funciones, que se han seleccionado cuidadosamente para manejar las funciones de manipulación de cadenas más comunes. stringi, por otro lado, esté diseñado para ser completo. Contiene casi todas las funciones que podamos necesitar: stringi tiene 234 funciones contra las 46 de stringr.

Si te encuentras luchando por hacer algo en stringr, vale la pena echarle un vistazo a stringi. Los paquetes funcionan de manera muy similar, por lo que deberías poder traducir el conocimiento de forma natural. La principal diferencia es el prefijo: str_ vs stri_.

forcats

En R, los factores se usan para trabajar con variables categóricas, variables que tienen un conjunto fijo y conocido de valores posibles. También son útiles cuando queremos mostrar vectores de caracteres en un orden no alfabético.

Históricamente, los factores eran mucho más fáciles de trabajar que los caracteres. Como resultado, muchas de las funciones de R base automáticamente convierten los caracteres en factores. Esto significa que los factores a menudo surgen en lugares donde no son realmente útiles.

Para trabajar con factores, usaremos el paquete forcats, que proporciona herramientas para trabajar con variables categóricas. Proporciona una amplia gama de ayudantes para trabajar con factores. forcats no es parte del tidyverse central, por lo que debemos cargarlo explícitamente.

library(tidyverse)
library(forcats)

Creando factores

Imaginemos que tenemos una variable que registra el mes:

x1 <- c("Dec", "Apr", "Jan", "Mar")

Usar una cadena para registrar esta variable tiene dos problemas:

  1. Solo hay doce meses posibles y no hay nada que nos salve de los errores tipográficos:
x2 <- c("Dec", "Apr", "Jam", "Mar")
  1. No ordena de una manera útil:
sort(x1)
## [1] "Apr" "Dec" "Jan" "Mar"

Podemos solucionar ambos problemas con un factor. Para crear un factor, debemos comenzar creando una lista de los niveles válidos:

month_levels <- c(
  "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
)

Ahora podemos crear un factor:

y1 <- factor(x1, levels = month_levels)
y1
## [1] Dec Apr Jan Mar
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
sort(y1)
## [1] Jan Mar Apr Dec
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

Y cualquier valor que no está en el conjunto se convertirá silenciosamente a NA:

y2 <- factor(x2, levels = month_levels)
y2
## [1] Dec  Apr  <NA> Mar 
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

Si omitimos los niveles, se tomarón de los datos en orden alfabético:

factor(x1)
## [1] Dec Apr Jan Mar
## Levels: Apr Dec Jan Mar

A veces preferiremos que el orden de los niveles coincida con el orden de la primera aparición en los datos. Podemos hacerlo al crear el factor estableciendo niveles a unique(x), o después del factor, con fct_inorder():

f1 <- factor(x1, levels = unique(x1))
f1
## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar
f2 <- x1 %>% factor() %>% fct_inorder()
f2
## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar

Si alguna vez necesitamos acceder al conjunto de niveles válidos directamente, podemos hacerlo con levels():

levels(f2)
## [1] "Dec" "Apr" "Jan" "Mar"

Encuesta social general

Usaremos forcats::gss_cat. Es una muestra de datos de la Encuesta Social General, que es una encuesta realizada en Estados unidos por la organización de investigación independiente NORC de la Universidad de Chicago. La encuesta tiene miles de preguntas, en gss_cat se encuentran algunas que ilustrarán algunos desafíos comunes que encontramos al trabajar con factores.

gss_cat

Cuando los factores se almacenan en un tibble, no podemos ver sus niveles tan fácilmente. Una forma de verlos es con count():

gss_cat %>%
  count(race)

O con una gráfica de barras:

ggplot(gss_cat, aes(race)) +
  geom_bar()

Por defecto, ggplot2 soltará niveles que no tienen ningún valor. Podemos obligarlo a mostrarlos con:

ggplot(gss_cat, aes(race)) +
  geom_bar() +
  scale_x_discrete(drop = FALSE)

Estos niveles representan valores válidos que simplemente no ocurrieron en este conjunto de datos.

Al trabajar con factores, las dos operaciones más comunes cambian el orden de los niveles y cambian los valores de los niveles.

Modificación de orden de los factores

A menudo es útil cambiar el orden de los niveles de los factores en una visualización. Por ejemplo, imagina que queremos explorar el promedio de horas dedicadas a ver televisión por día en diferentes religiones:

relig_summary <- gss_cat %>%
  group_by(relig) %>%
  summarise(
    age = mean(age, na.rm = TRUE),
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )

ggplot(relig_summary, aes(tvhours, relig)) + geom_point()

Es difícil interpretar esta gráfica porque no hay un patrón general. Podemos mejorarlo reordenando los niveles de relig usando fct_reorder(). fct_reorder() toma tres argumentos:

  • f, el factor cuyos niveles queremos modificar
  • x, un vector numérico que deseamos usar para reordenar los niveles.
  • Opcionalmente, fun una función que se usa si hay múltiples valores de x para cada valor de f. El valor predeterminado es median.
ggplot(relig_summary, aes(tvhours, fct_reorder(relig, tvhours))) +
  geom_point()

Reordenar la religión hace que sea mucho más fácil ver que las personas de la categoría “No sé” ven mucha más televisión, y el Hinduismo y otras religiones orientales ven mucho menos.

A medida que comenzamos a hacer transformaciones más complicadas, es recomendable moverlas de aes() a un mutate() por separado. Por ejemplo, podríamos reescribir la gráfica anterior como:

relig_summary %>%
  mutate(relig = fct_reorder(relig, tvhours)) %>%
  ggplot(aes(tvhours, relig)) +
    geom_point()

¿Qué pasa si creamos una gráfica similar mirando cómo varía la edad promedio a través del nivel de ingresos reportados?

rincome_summary <- gss_cat %>%
  group_by(rincome) %>%
  summarise(
    age = mean(age, na.rm = TRUE),
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )

ggplot(rincome_summary, aes(age, fct_reorder(rincome, age))) + geom_point()

¡Aquí, reordenar arbitrariamente los niveles no es una buena idea! Eso es porque rincome ya tiene un orden principal con el que no deberíamos meternos. Reservemos fct_reorder() para factores cuyos niveles se ordenan arbitrariamente.

Sin embargo, tiene sentido tirar “No aplicable” al frente con los otros niveles especiales. Podemos usar fct_relevel(). Toma un factor, f y luego cualquier cantidad de niveles que deseemos mover al frente de la línea.

ggplot(rincome_summary, 
       aes(age, fct_relevel(rincome, "Not applicable")))
+ geom_point()

¿Por qué crees que la edad promedio para “No aplicable” es tan alta?

Otro tipo de reordenamiento es útil cuando se colorean las líneas en un diagrama. fct_reorder2() reordena el factor por los valores y asociados con los valores x más grandes. Esto hace que la gráfica sea más fácil de leer porque los colores de la línea se alinean con la leyenda.

by_age <- gss_cat %>%
  filter(!is.na(age)) %>%
  count(age, marital) %>%
  group_by(age) %>%
  mutate(prop = n / sum(n))

ggplot(by_age, aes(age, prop, colour = marital)) +
  geom_line(na.rm = TRUE)

ggplot(by_age, 
       aes(age, prop, colour = fct_reorder2(marital, age, prop))) +
  geom_line() +
  labs(colour = "marital")

Finalmente, para gráficas de barra, podemos usar fct_infreq() para ordenar niveles en frecuencia creciente: este es el tipo más simple de reordenamiento porque no necesita ninguna variable adicional. Es posible que necesitemos combinar con fct_rev().

gss_cat %>%
  mutate(marital = marital %>% fct_infreq() %>% fct_rev()) %>%
  ggplot(aes(marital)) +
    geom_bar()

Modificación de niveles de factores

Más poderoso que cambiar las órdenes de los niveles es cambiar sus valores. Esto nos permite aclarar las etiquetas para nuestra publicación y los niveles de contracción para las pantallas de alto nivel. La herramienta más general y poderosa es fct_recode(). Nos permite recodificar o cambiar el valor de cada nivel. Por ejemplo, tomemos el gss_cat$partyid:

gss_cat %>% count(partyid)

Los niveles son escuetos e inconsistentes. Vamos a ajustarlos para que sean más largos y usar una construcción paralela.

gss_cat %>%
  mutate(partyid = fct_recode(partyid,
    "Republican, strong"    = "Strong republican",
    "Republican, weak"      = "Not str republican",
    "Independent, near rep" = "Ind,near rep",
    "Independent, near dem" = "Ind,near dem",
    "Democrat, weak"        = "Not str democrat",
    "Democrat, strong"      = "Strong democrat"
  )) %>%
  count(partyid)

fct_recode() dejará los niveles que no se mencionan explícitamente tal como están y nos advertirá si nos estamos refiriendo accidentalmente a un nivel que no existe.

Para combinar grupos, podemos asignar múltiples niveles antiguos al mismo nivel nuevo:

gss_cat %>%
  mutate(partyid = fct_recode(partyid,
    "Republican, strong"    = "Strong republican",
    "Republican, weak"      = "Not str republican",
    "Independent, near rep" = "Ind,near rep",
    "Independent, near dem" = "Ind,near dem",
    "Democrat, weak"        = "Not str democrat",
    "Democrat, strong"      = "Strong democrat",
    "Other"                 = "No answer",
    "Other"                 = "Don't know",
    "Other"                 = "Other party"
  )) %>%
  count(partyid)

Debemos usar esta técnica con cuidado: si agrupamos categorías que son realmente diferentes, terminaremos con resultados engañosos.

Si queremos colapsar muchos niveles, fct_collapse() es una variante útil de fct_recode(). Para cada nueva variable, podemos proporcionar un vector de niveles antiguos:

gss_cat %>%
  mutate(partyid = fct_collapse(partyid,
    other = c("No answer", "Don't know", "Other party"),
    rep = c("Strong republican", "Not str republican"),
    ind = c("Ind,near rep", "Independent", "Ind,near dem"),
    dem = c("Not str democrat", "Strong democrat")
  )) %>%
  count(partyid)

A veces solo queremos agrupar a todos los grupos pequeños para simplificar un diagrama o una tabla. Ese es el trabajo de fct_lump():

gss_cat %>%
  mutate(relig = fct_lump(relig)) %>%
  count(relig)

El comportamiento predeterminado es agrupar progresivamente los grupos más pequeños, asegurando que el agregado sigue siendo el grupo más pequeño. En este caso no es muy útil: es cierto que la mayoría de los estadounidenses en esta encuesta son protestantes, pero es probable colapsemos de más.

En cambio, podemos usar el parámetro n para especificar cuántos grupos (excluyendo otros) queremos mantener:

gss_cat %>%
  mutate(relig = fct_lump(relig, n = 10)) %>%
  count(relig, sort = TRUE) %>%
  print(n = Inf)
## # A tibble: 10 x 2
##    relig                       n
##    <fct>                   <int>
##  1 Protestant              10846
##  2 Catholic                 5124
##  3 None                     3523
##  4 Christian                 689
##  5 Other                     458
##  6 Jewish                    388
##  7 Buddhism                  147
##  8 Inter-nondenominational   109
##  9 Moslem/islam              104
## 10 Orthodox-christian         95

lubridate

A primera vista, las fechas y las horas parecen simples. Los usamos todo el tiempo en la vida normal, y no parecen causar mucha confusión. Sin embargo, cuanto más aprendes sobre fechas y horarios, más complicados parecen obtenerlos. Para calentar, probemos estas tres preguntas aparentemente simples:

  • ¿Todos los años tienen 365 días?
  • ¿Todos los días tienen 24 horas?
  • ¿Cada minuto tiene 60 segundos?

Sabemos que no todos los años tienen 365 días, pero ¿conoces la regla completa para determinar si un año es bisiesto? Es posible que recordemos que muchas partes del mundo usan el horario de verano (DST), por lo que algunos días tienen 23 horas y otros tienen 25. Es posible que no sepamos que algunos minutos tienen 61 segundos porque de vez en cuando se agregan segundos intercalares porque la rotación de la Tierra se está desacelerando gradualmente.

Las fechas y los tiempos son difíciles porque tienen que conciliar dos fenómenos físicos (la rotación de la Tierra y su órbita alrededor del sol) con una gran cantidad de fenómenos geopolíticos que incluyen meses, zonas horarias y horario de verano.

Usaremos el paquete lubridate, que facilita el trabajo con fechas y horas en R. lubridate no forma parte del tidyverse. Usaremos la base que ya conocemos nycflights13 para practicar.

library(tidyverse)
library(lubridate)
library(nycflights13)

Creación de fecha/hora

Hay tres tipos de datos de fecha/hora que se refieren a un instante en el tiempo:

  • Una fecha. Tibbles imprime esto como <date>.

  • Una hora del día. Tibbles imprime esto como <time>.

  • Una fecha-hora es una fecha más una hora: identifica de manera única un instante en el tiempo (generalmente al segundo más cercano). Tibbles imprime esto como <dttm>.

Siempre debemos usar el tipo de datos más simple posible que funcione para nuestras necesidades. Eso significa que si podemos usar una fecha en lugar de una fecha-hora, deberíamos hacerlo. Los horarios son sustancialmente más complicados debido a la necesidad de manejar zonas horarias.

Para obtener la fecha actual o la fecha-hora, usamos today() o now():

today()
## [1] "2018-08-02"
now()
## [1] "2018-08-02 17:11:04 CDT"

De lo contrario, hay tres formas en las que es probable que creemos una fecha/hora:

  • De una cadena de texto.
  • A partir de componentes individuales de fecha-hora.
  • Desde un objeto de fecha/hora existente.

Funcionan de la siguiente manera.

De cadenas

Los datos de fecha/hora a menudo vienen como cadenas. Podemos usar los ayudantes provistos por lubridate para transformarlas en formato de fecha-hora. Automáticamente resuelven el formato una vez que especificamos el orden del componente. Para usarlos, debemos identificar el orden en que aparecen el año, el mes y el día en las fechas, luego organizar “y”, “m” y “d” en el mismo orden. Eso nos da el nombre de la funci´n de lubridate que parseara la fecha. Por ejemplo:

ymd("2017-01-31")
## [1] "2017-01-31"
mdy("January 31st, 2017")
## [1] "2017-01-31"
dmy("31-Jan-2017")
## [1] "2017-01-31"

Estas funciones también toman números sin comillas. Esta es la forma más concisa de crear un único objeto de fecha/hora, como puede ser necesario al filtrar datos de fecha/hora. ymd() es corto e inequívoco:

ymd(20170131)
## [1] "2017-01-31"

ymd() y sus amigos crean fechas. Para crear una fecha y hora, agregamos un guión bajo y una o más de “h”, “m” y “s” al nombre de la función de análisis sintáctico:

ymd_hms("2017-01-31 20:11:59")
## [1] "2017-01-31 20:11:59 UTC"
mdy_hm("01/31/2017 08:01")
## [1] "2017-01-31 08:01:00 UTC"

También podemos forzar la creación de una fecha y hora a partir de una fecha proporcionando una zona horaria:

ymd(20170131, tz = "UTC")
## [1] "2017-01-31 UTC"

De componentes individuales

En lugar de una sola cadena, a veces tendremos los componentes individuales de la fecha y hora distribuidos en varias columnas. Esto es lo que tenemos en los datos de vuelos:

flights %>% 
  select(year, month, day, hour, minute)

Para crear una fecha/hora a partir de este tipo de entrada, usamos make_date() para fechas o make_datetime() fechas-horas:

flights %>% 
  select(year, month, day, hour, minute) %>% 
  mutate(departure = make_datetime(year, month, day, hour, minute))

Hagamos lo mismo para cada una de las cuatro columnas de tiempo en flights. Los tiempos están representados en un formato ligeramente extraño, por lo que usamos la aritmática de módulo para extraer los componentes de hora y minuto.

make_datetime_100 <- function(year, month, day, time) {
  make_datetime(year, month, day, time %/% 100, time %% 100)
}

flights_dt <- flights %>% 
  filter(!is.na(dep_time), !is.na(arr_time)) %>% 
  mutate(
    dep_time = make_datetime_100(year, month, day, dep_time),
    arr_time = make_datetime_100(year, month, day, arr_time),
    sched_dep_time = make_datetime_100(year, month, day, sched_dep_time),
    sched_arr_time = make_datetime_100(year, month, day, sched_arr_time)
  ) %>% 
  select(origin, dest, ends_with("delay"), ends_with("time"))

flights_dt

Con estos datos, podemos visualizar la distribución de los horarios de salida a lo largo del año:

flights_dt %>% 
  ggplot(aes(dep_time)) + 
  geom_freqpoly(binwidth = 86400) # 86400 segundos = 1 día

O en un solo día:

flights_dt %>% 
  filter(dep_time < ymd(20130102)) %>% 
  ggplot(aes(dep_time)) + 
  geom_freqpoly(binwidth = 600) # 600 s = 10 minutos

Notemos que cuando utilizamos fechas y horas en un contexto numérico (como en un histograma), 1 significa 1 segundo, por lo que un ancho de 86400 equivale a un día. Para las fechas, 1 significa 1 día.

De otros tipos

Es posible que deseemos cambiar entre una fecha-hora y una fecha. Para eso, usamos as_datetime() y as_date():

as_datetime(today())
## [1] "2018-08-02 UTC"
as_date(now())
## [1] "2018-08-02"

Algunas veces obtendremos fecha/hora como compensaciones numéricas del “Unix Epoch”, 1970-01-01. Si el desplazamiento está en segundos, usamos as_datetime() si es en días, usamos as_date().

as_datetime(60 * 60 * 10)
## [1] "1970-01-01 10:00:00 UTC"
as_date(365 * 10 + 2)
## [1] "1980-01-01"

Componentes de fecha y hora

Ahora que sabemos cómo obtener datos de fecha y hora en las estructuras de datos de fecha y hora de R, exploremos qué podemos hacer con ellos.

Obteniendo componentes

Podemos sacar las piezas individuales de la fecha con las funciones de acceso year(), month(), mday() (día del mes), yday() (los días del año), wday() (día de la semana), hour(), minute(), y second().

datetime <- ymd_hms("2016-07-08 12:34:56")

year(datetime)
## [1] 2016
month(datetime)
## [1] 7
mday(datetime)
## [1] 8
yday(datetime)
## [1] 190
wday(datetime)
## [1] 6

Para month() y wday() podemos configurar label = TRUE para devolver el nombre abreviado del mes o el día de la semana. También podemos establecer abbr = FALSE para devolver el nombre completo.

month(datetime, label = TRUE)
## [1] Jul
## 12 Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < ... < Dec
wday(datetime, label = TRUE, abbr = FALSE)
## [1] Friday
## 7 Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < ... < Saturday

Podemos utilizar wday() para ver que más vuelos salen durante la semana que en el fin de semana:

flights_dt %>% 
  mutate(wday = wday(dep_time, label = TRUE)) %>% 
  ggplot(aes(x = wday)) +
    geom_bar()

Hay un patrón interesante si observamos el retraso promedio de salida por minuto dentro de una hora. ¡Parece que los vuelos que salen en minutos 20-30 y 50-60 tienen retrasos mucho más bajos que el resto de la hora!

flights_dt %>% 
  mutate(minute = minute(dep_time)) %>% 
  group_by(minute) %>% 
  summarise(
    avg_delay = mean(arr_delay, na.rm = TRUE),
    n = n()) %>% 
  ggplot(aes(minute, avg_delay)) +
    geom_line()

Curiosamente, si vemos la hora programada de salida, no vemos un patrón tan fuerte:

sched_dep <- flights_dt %>% 
  mutate(minute = minute(sched_dep_time)) %>% 
  group_by(minute) %>% 
  summarise(
    avg_delay = mean(arr_delay, na.rm = TRUE),
    n = n())

ggplot(sched_dep, aes(minute, avg_delay)) +
  geom_line()

Entonces, ¿por qué vemos ese patrón con los tiempos reales de salida? Bueno, al igual que muchos datos recopilados por humanos, hay un fuerte sesgo hacia los vuelos que salen en tiempos de salida “agradables”. Siempre debemos estar atentos a este tipo de patrón cuando trabajemos con datos que involucren el juicio humano.

ggplot(sched_dep, aes(minute, n)) +
  geom_line()

Redondeo

Un enfoque alternativo para graficar los componentes individuales es redondear la fecha a una unidad cercana de tiempo, con floor_date(), round_date(), y ceiling_date(). Cada función toma un vector de fechas para ajustar y redondear. Esto, por ejemplo, nos permite graficar el número de vuelos por semana:

flights_dt %>% 
  count(week = floor_date(dep_time, "week")) %>% 
  ggplot(aes(week, n)) +
    geom_line()

Configuración de componentes

También podemos usar cada función para configurar los componentes de una fecha/hora:

(datetime <- ymd_hms("2016-07-08 12:34:56"))
## [1] "2016-07-08 12:34:56 UTC"
year(datetime) <- 2020
datetime
## [1] "2020-07-08 12:34:56 UTC"
month(datetime) <- 01
datetime
## [1] "2020-01-08 12:34:56 UTC"
hour(datetime) <- hour(datetime) + 1
datetime
## [1] "2020-01-08 13:34:56 UTC"

Alternativamente, en lugar de modificar, podemos crear una nueva fecha y hora con update(). Esto también nos permite establecer múltiples valores a la vez.

update(datetime, year = 2020, month = 2, mday = 2, hour = 2)
## [1] "2020-02-02 02:34:56 UTC"

Si los valores son demasiado grandes, se reiniciarán:

ymd("2015-02-01") %>% 
  update(mday = 30)
## [1] "2015-03-02"
ymd("2015-02-01") %>% 
  update(hour = 400)
## [1] "2015-02-17 16:00:00 UTC"

Podemos usar update() para mostrar la distribución de vuelos a lo largo del día para cada día del año:

flights_dt %>% 
  mutate(dep_hour = update(dep_time, yday = 1)) %>% 
  ggplot(aes(dep_hour)) +
    geom_freqpoly(binwidth = 300)

Lapso de tiempo

Veremos tres clases importantes que representan períodos de tiempo:

  • durations, que representan una cantidad exacta de segundos.
  • periods, que representan unidades humanas como semanas y meses.
  • intervals, que representan un punto inicial y final.

Duraciones

En R, cuando restamos dos fechas, obtenemos un objeto de tiempo difftime:

age <- today() - ymd(19880813)
age
## Time difference of 10946 days

Un objeto de clase difftime registra un lapso de tiempo de segundos, minutos, horas, días o semanas. Esta ambigüedad puede hacer que sea un poco doloroso trabajar con él, por lo que lubridate proporciona una alternativa que siempre usa segundos: la duración.

as.duration(age)
## [1] "945734400s (~29.97 years)"

Las duraciones vienen con un grupo de constructores convenientes:

dseconds(15)
## [1] "15s"
dminutes(10)
## [1] "600s (~10 minutes)"
dhours(c(12, 24))
## [1] "43200s (~12 hours)" "86400s (~1 days)"
ddays(0:5)
## [1] "0s"                "86400s (~1 days)"  "172800s (~2 days)"
## [4] "259200s (~3 days)" "345600s (~4 days)" "432000s (~5 days)"
dweeks(3)
## [1] "1814400s (~3 weeks)"
dyears(1)
## [1] "31536000s (~52.14 weeks)"

Las duraciones siempre registran el lapso de tiempo en segundos. Las unidades más grandes se crean mediante la conversión de minutos, horas, días, semanas y años a segundos a las tasas estándar (60 segundos en un minuto, 60 minutos en una hora, 24 horas en día, 7 días a la semana, los 365 días en un año).

Podemos sumar y multiplicar duraciones:

2 * dyears(1)
## [1] "63072000s (~2 years)"
dyears(1) + dweeks(12) + dhours(15)
## [1] "38847600s (~1.23 years)"

Podemos sumar y restar duraciones desde y a días:

tomorrow <- today() + ddays(1)
last_year <- today() - dyears(1)

Sin embargo, dado que las duraciones representan una cantidad exacta de segundos, a veces podemos obtener un resultado inesperado:

one_pm <- ymd_hms("2016-03-12 13:00:00", tz = "America/New_York")
one_pm
## [1] "2016-03-12 13:00:00 EST"
one_pm + ddays(1)
## [1] "2016-03-13 14:00:00 EDT"

¿Por qué un día después de la 1 pm el 12 de marzo no da las 2 pm el 13 de marzo? Si observamos detenidamente la fecha, también podemos notar que las zonas horarias han cambiado. Debido al horario de verano, el 12 de marzo solo tiene 23 horas, por lo que si agregamos días en segundos, terminamos con un horario diferente.

Períodos

Para resolver este problema, lubridate proporciona periodos. Los períodos son períodos de tiempo, pero no tienen una duración fija en segundos, sino que funcionan con tiempos “humanos”, como días y meses. Eso les permite trabajar de una manera más intuitiva:

one_pm
## [1] "2016-03-12 13:00:00 EST"
one_pm + days(1)
## [1] "2016-03-13 13:00:00 EDT"

Al igual que las duraciones, los períodos se pueden crear con varias funciones amigables.

seconds(15)
## [1] "15S"
minutes(10)
## [1] "10M 0S"
hours(c(12, 24))
## [1] "12H 0M 0S" "24H 0M 0S"
days(7)
## [1] "7d 0H 0M 0S"
months(1:6)
## [1] "1m 0d 0H 0M 0S" "2m 0d 0H 0M 0S" "3m 0d 0H 0M 0S" "4m 0d 0H 0M 0S"
## [5] "5m 0d 0H 0M 0S" "6m 0d 0H 0M 0S"
weeks(3)
## [1] "21d 0H 0M 0S"
years(1)
## [1] "1y 0m 0d 0H 0M 0S"

Podemos sumar y multiplicar periodos:

10 * (months(6) + days(1))
## [1] "60m 10d 0H 0M 0S"
days(50) + hours(25) + minutes(2)
## [1] "50d 25H 2M 0S"

Y, por supuesto, sumarlos a las fechas. En comparación con las duraciones, los períodos tienen más probabilidades de hacer lo que esperamos:

# Un año bisiesto
ymd("2016-01-01") + dyears(1)
## [1] "2016-12-31"
ymd("2016-01-01") + years(1)
## [1] "2017-01-01"
# Horario de Verano
one_pm + ddays(1)
## [1] "2016-03-13 14:00:00 EDT"
one_pm + days(1)
## [1] "2016-03-13 13:00:00 EDT"

Usemos períodos para arreglar una rareza relacionada con nuestras fechas de vuelo. Algunos aviones parecen haber llegado a su destino antes de partir de la ciudad de Nueva York.

flights_dt %>% 
  filter(arr_time < dep_time)

Estos son vuelos nocturnos. Utilizamos la misma información de fecha tanto para la hora de salida como para la hora de llegada, pero estos vuelos llegaron al día siguiente. Podemos solucionar esto añadiendo days(1) al tiempo de llegada de cada vuelo nocturno.

flights_dt <- flights_dt %>% 
  mutate(
    overnight = arr_time < dep_time,
    arr_time = arr_time + days(overnight * 1),
    sched_arr_time = sched_arr_time + days(overnight * 1)
  )

Ahora todos nuestros vuelos obedecen las leyes de la física.

flights_dt %>% 
  filter(overnight, arr_time < dep_time)

Intervalos

Es obvio lo que dyears(1) / ddays(365) debería regresar: uno, porque las duraciones siempre están representadas por una cantidad de segundos, y una duración de un año se define como 365 días en segundos.

¿Qué debería regresar years(1) / days(1)?

Si el año fue 2015 debería devolver 365, pero si fuera 2016, ¡debería devolver 366! No hay suficiente información para que lubridate brinde una única respuesta clara. Lo que hace en cambio es dar una estimación, con una advertencia:

years(1) / days(1)
## estimate only: convert to intervals for accuracy
## [1] 365.25

Si queremos una medida más precisa, usamos un intervalo. Un intervalo es una duración con un punto de inicio, eso lo hace preciso para que podamos determinar exactamente cuánto tiempo es:

next_year <- today() + years(1)
(today() %--% next_year) / ddays(1)
## [1] 365

Para saber cuántos períodos entran en un intervalo, debemos usar la división de enteros:

(today() %--% next_year) %/% days(1)
## [1] 365

Resumen

¿Cómo se elige entre duración, períodos e intervalos? Como siempre, elegimos la estructura de datos más simple que resuelva nuestro problema. Si solo nos importa el tiempo físico, usamos una duración; si necesitamos agregar tiempos humanos, usamos un período; si necesitamos saber cuánto tiempo dura un lapso en unidades humanas, usamos un intervalo.

Las operaciones aritméticas permitidas entre pares de clases de fecha/hora:

Zonas horarias

Las zonas horarias son un tema enormemente complicado debido a su interacción con entidades geopolíticas. Afortunadamente, no necesitamos profundizar en todos los detalles, ya que no son todos importantes para el análisis de datos, pero hay algunos desafíos que debemos enfrentar.

El primer desafío es que los nombres diarios de las zonas horarias tienden a ser ambiguos. Por ejemplo, los estadounidenses, están familiarizados con EST o Eastern Standard Time. Sin embargo, tanto Australia como Canadá también tienen EST. Para evitar confusiones, R usa las zonas horarias estándar internacionales de IANA. Estos usan un esquema de nombres consistente "/", típicamente en la forma "<continente>/<ciudad>" (hay algunas excepciones porque no todos los países se encuentran en un continente). Los ejemplos incluyen "America/New_York", "Europe/Paris" y "Pacific/Auckland".

Podemos descubrir lo que piensa R que es nuestra zona horaria actual con Sys.timezone():

Sys.timezone()
## [1] "America/Mexico_City"

(Si R no sabe, obtendremos un NA.)

Vemos la lista completa de todos los nombres de zona horaria con OlsonNames():

length(OlsonNames())
## [1] 592
head(OlsonNames())
## [1] "Africa/Abidjan"     "Africa/Accra"       "Africa/Addis_Ababa"
## [4] "Africa/Algiers"     "Africa/Asmara"      "Africa/Asmera"

En R, la zona horaria es un atributo de la fecha y hora que solo controla la impresión. Por ejemplo, estos tres objetos representan el mismo instante en el tiempo:

(x1 <- ymd_hms("2015-06-01 12:00:00", tz = "America/New_York"))
## [1] "2015-06-01 12:00:00 EDT"
(x2 <- ymd_hms("2015-06-01 18:00:00", tz = "Europe/Copenhagen"))
## [1] "2015-06-01 18:00:00 CEST"
(x3 <- ymd_hms("2015-06-02 04:00:00", tz = "Pacific/Auckland"))
## [1] "2015-06-02 04:00:00 NZST"

Podemos verificar que son el mismo tiempo restando:

x1 - x2
## Time difference of 0 secs
x1 - x3
## Time difference of 0 secs

A menos que se especifique lo contrario, lubridate siempre usa UTC. UTC (tiempo universal coordinado) es la zona horaria estándar utilizada por la comunidad científica y más o menos equivalente a su predecesor, GMT (Greenwich Mean Time). No tiene horario de verano, lo que la hace una representación conveniente para cálculos. Las operaciones que combinan fechas y horas, como c(), a menudo ignorarón la zona horaria. En ese caso, los horarios se mostrarán en nuestra zona horaria local:

x4 <- c(x1, x2, x3)
x4
## [1] "2015-06-01 11:00:00 CDT" "2015-06-01 11:00:00 CDT"
## [3] "2015-06-01 11:00:00 CDT"

Podemos cambiar la zona horaria de dos maneras:

  • Mantener el mismo instante en tiempo y cambiar la forma en que se muestra. Usamos esto cuando el instante es correcto, pero deseamos una visualización más natural.
x4a <- with_tz(x4, tzone = "Australia/Lord_Howe")
x4a
## [1] "2015-06-02 02:30:00 +1030" "2015-06-02 02:30:00 +1030"
## [3] "2015-06-02 02:30:00 +1030"
x4a - x4
## Time differences in secs
## [1] 0 0 0
  • Cambiar el instante subyacente en tiempo. Usamos esto cuando tenemos un instante que haya sido etiquetado con la zona horaria incorrecta y necesitamos repararlo.
x4b <- force_tz(x4, tzone = "Australia/Lord_Howe")
x4b
## [1] "2015-06-01 11:00:00 +1030" "2015-06-01 11:00:00 +1030"
## [3] "2015-06-01 11:00:00 +1030"
x4b - x4
## Time differences in hours
## [1] -15.5 -15.5 -15.5

Ejercicios

stringr

  1. A menudo verás paste() y paste0(). ¿Cuál es la diferencia entre las dos funciones? ¿A quí función de stringr son equivalentes? ¿Cómo difieren las funciones en su manejo de NA?

  2. En tus propias palabras, describe la diferencia entre los argumentos sep y collapse de str_c().

  3. Usa str_length() y str_sub() para extraer el caracter medio de una cadena. ¿Qué harás si la cadena tiene un número par de caracteres?

  4. ¿Qué hace str_wrap()? ¿Cuándo podrías querer usarlo?

  5. ¿Qué hace str_trim()? ¿Qué es lo opuesto a str_trim()?

  6. Escribe una función que convierta (por ejemplo) un vector c("a", "b", "c") en la cadena a, b y c. Piensa con cuidado sobre lo que debería hacer esta función si recibe un vector de longitud 0, 1 o 2.

  7. Explica por qué cada una de estas cadenas no coinciden con un \: "\", "\\", "\\\".

  8. ¿Cómo coincidirías la secuencia "'\?

  9. ¿Qué patrones encontrará la expresión regular \..\..\..? ¿Cómo lo representarías como una cadena?

  10. ¿Cómo coincidirías con la cadena literal "$^$"?

  11. Dado el corpus de palabras en stringr::words, crea expresiones regulares que encuentren todas las palabras que:

    • Comienzan con “y”.
    • Terminan con “x”
    • Tienen exactamente tres letras de largo. (¡No hagas trampa al usar str_length()!)
    • Tener siete letras o más.

Dado que esta lista es larga, es posible que desees utilizar el argumento match de str_view() para mostrar solo las palabras coincidentes o no coincidentes.

  1. Crea expresiones regulares para encontrar todas las palabras que:

    • Comienzan con una vocal.
    • Solo contienen consonantes. (Sugerencia: piensa en encontrar “no” -vocales).
    • Terminan con ed, pero no con eed.
    • Terminan con ing o ise.
  2. Crea una expresión regular que coincida con los números de teléfono escritos comúnmente en tu país.

  3. Describe los equivalentes de ?, +, * en la forma {m,n}.

  4. Crea expresiones regulares para encontrar todas las palabras que:

    • Comienzan con tres consonantes.
    • Tienen tres o más vocales seguidas.
    • Tienen dos o más parejas de vocales y consonantes seguidas.
    • Resuelve los crucigramas de la expresión regular para principiantes en https://regexcrossword.com/challenges/beginner.
  5. Construya expresiones regulares para unir palabras que:

    • Comienzan y terminan con el mismo caracter.
    • Contienen un par de letras repetidas (por ejemplo, “church” contiene “ch” repetido dos veces).
    • Contienen una letra repetida en al menos tres lugares (por ejemplo, “eleven” contiene tres “e”).
  6. Para cada uno de los siguientes desafíos, intenta resolverlo utilizando una sola expresión regular y una combinación de llamadas múltiples a str_detect().

    • Encuentra todas las palabras que comienzan o terminan llamadas múltiplesx.
    • Encuentra todas las palabras que comienzan con una vocal y terminan con una consonante.
    • ¿Hay alguna palabra que contenga al menos una de cada vocal diferente?
  7. ¿Qué palabra tiene el mayor número de vocales? ¿Qué palabra tiene la mayor proporción de vocales? (Sugerencia: ¿cuál es el denominador?)

  8. Encuentra todas las palabras que vienen después de un “número” como “uno”, “dos”, “tres”, etc. Saca el número y la palabra (en inglés).

  9. Encuentra todas las contracciones. Separa las piezas antes y después del apóstrofe.

  10. Reemplaza todas las barras diagonales en una cadena con barras diagonales inversas.

  11. Implementa una versión simple de str_to_lower() usando replace_all().

  12. Cambia la primera y la última letra en words. ¿Cuáles de esas cadenas son todavía palabras?

  13. Divide una cadena como “apples, pears, and bananas” en componentes individuales.

  14. ¿Por qué es mejor dividir con boundary("word") que con " "?

  15. ¿Qué hace la división con una cadena vacía ("")? Experimenta y luego lee la documentación.

  16. ¿Cuáles son las cinco palabras más comunes en sentences?

  17. Encuentra las funciones de stringi que:

    • Cuenta el número de palabras.
    • Encuentra cadenas duplicadas.
    • Genera texto aleatorio.
  18. ¿Cómo controlas el lenguaje que stri_sort() usa para ordenar?

forcats

  1. Explora la distribución de rincome en forcats::gss_cat (ingreso reportado). ¿Qué hace que la gráfica de barras predeterminada sea difícil de entender? ¿Cómo podrías mejorarla?

  2. ¿Cuál es el más común relig en esta encuesta? ¿Cuál es el más común partyid?

  3. Hay algunos números sospechosamente altos en tvhours. ¿Es la media un buen resumen?

  4. Para cada factor en gss_cat identifica si el orden de los niveles es arbitrario o principal.

  5. ¿Por qué mover “No aplicable” de rincome al frente de los niveles lo mueve al final de la gráfica?

  6. ¿Cómo han cambiado con el tiempo las proporciones de personas que se identifican como demócratas, republicanos e independientes?

  7. ¿Cómo podrías colapsar rincome en un pequeño conjunto de categorías?

lubridate

  1. ¿Qué sucede si parseas una cadena que contiene fechas no válidas?
ymd(c("2010-10-10", "bananas"))
  1. ¿Qué hace el argumento tzone para today()? ¿Por qué es importante?

  2. Use la función de lubridate apropiada para parsear cada una de las siguientes fechas:

d1 <- "January 1, 2010"
d2 <- "2015-Mar-07"
d3 <- "06-Jun-2017"
d4 <- c("August 19 (2015)", "July 1 (2015)")
d5 <- "12/30/14" # 30 Dic, 2014
  1. ¿Cómo cambia la distribución de los tiempos de vuelo en un día a lo largo del año?

  2. Compara dep_time, sched_dep_time y dep_delay. ¿Son consistentes? Explica tus hallazgos

  3. Compara air_time con la duración entre la partida y la llegada. Explica tus hallazgos (Sugerencia: considera la ubicación del aeropuerto).

  4. ¿Cómo cambia el tiempo promedio de demora en el transcurso de un día? ¿Deberías usar dep_time o sched_dep_time? ¿Por qué?

  5. ¿En qué día de la semana deberías irte si quieres minimizar la posibilidad de un retraso?

  6. ¿Qué hace la distribución de diamonds$carat y flights$sched_dep_time similares?

  7. Confirma la hipñótesis de que las salidas anticipadas de los vuelos en los minutos 20-30 y 50-60 son causadas por vuelos programados que salen temprano. Sugerencia: crea una variable binaria que te diga si un vuelo se retrasó o no.

  8. ¿Cómo explicarías days(overnight * 1) a alguien que acaba de comenzar a aprender R? ¿Cómo funciona?

  9. Crea un vector de fechas para el primer día de cada mes en 2015. Crea un vector de fechas que indique el primer día de cada mes en el año actual.

  10. Escribe una función que, dada tu fecha de cumpleaños (como fecha), devuelva la edad que tienes en años.

  11. ¿Por qué no funciona (today() %--% (today() + years(1)) / months(1)?

Respuestas CarlosFVG

stringr

library(stringr)
library(tidyverse)
  1. A menudo verás paste() y paste0(). ¿Cuál es la diferencia entre las dos funciones? ¿A qué función de stringr son equivalentes? ¿Cómo difieren las funciones en su manejo de NA?

La diferencia entre paste() y paste0() es que paste() tiene por default el argumento separated como " " (Notar el espacio) y paste0() como “” (sin espacio)

Por ejemplo :

c1 <- "Esta es la primera frase"
c2 <- "Esta es la segunda"
paste(c1,c2)
## [1] "Esta es la primera frase Esta es la segunda"
paste0(c1,c2)
## [1] "Esta es la primera fraseEsta es la segunda"

La función equivalente en stringr es str_c()

paste() y paste0() concatena el valor lógico NA como caracter:

paste(c1, NA)
## [1] "Esta es la primera frase NA"

En cambio, str_c() hace lo siguiente:

str_c(c1,NA)
## [1] NA

Además, cuando se desea concatenar los elementos de un vector y uno de ellos es NA, str_c no imprime “NA” como lo hacen paste() y paste0()

x <- c("hola", "adios", NA)
str_c(x)
## [1] "hola"  "adios" NA
paste(x)
## [1] "hola"  "adios" "NA"
paste0(x)
## [1] "hola"  "adios" "NA"
  1. En tus propias palabras, describe la diferencia entre los argumentos sep y collapse de str_c().

el argumento sep separa las palabras sin importar si el caracter asignado para separar esta dentro de alguno de los caracteres, en cambio, collapse toma aquel caracter para tomarlo como separador, por ejemplo:

c3 <- "Esta cadena tiene un '-' -"
str_c(c3, c1, sep  = "-")
## [1] "Esta cadena tiene un '-' --Esta es la primera frase"
str_c(c3, c1, collapse  = "-")
## [1] "Esta cadena tiene un '-' -Esta es la primera frase"
  1. Usa str_length() y str_sub() para extraer el caracter medio de una cadena. ¿Qué harás si la cadena tiene un número par de caracteres?
c4 <- "Un numero impar de letras"
str_sub(c4, str_length(c4)/2, str_length(c4)/2)
## [1] "m"

Para un número par de caracteres par, se tomará el caracter que resulte de la operación:

\[\dfrac{str\_length(cadena)}{2}\] y el caracter siguiente, por ejemplo:

c5 <- "Un numero par_*de caracteres"
str_sub(c5, str_length(c5)/2, (str_length(c5)/2)+1)
## [1] "_*"
  1. ¿Qué hace str_wrap()? ¿Cuándo podrías querer usarlo?

str_wrap() nos ayuda a darle formato a un texto sobre los saltos de linea, un ejemplo lo proporciona la documentación de R : ? str_wrap

thanks_path <- file.path(R.home("doc"), "THANKS")
thanks <- str_c(readLines(thanks_path), collapse = "\n")
thanks <- word(thanks, 1, 3, fixed("\n\n"))
cat(str_wrap(thanks), "\n")
cat(str_wrap(thanks, width = 40), "\n")
cat(str_wrap(thanks, width = 60, indent = 2), "\n")
cat(str_wrap(thanks, width = 60, exdent = 2), "\n")
cat(str_wrap(thanks, width = 0, exdent = 2), "\n")
  1. ¿Qué hace str_trim()? ¿Qué es lo opuesto a str_trim()?

la función str_trim() elimina los espacios en blanco al principio y al final de una cadena de caracteres, por ejemplo :

str_trim("  Antes había espacios en blanco al inicio y al final     ")
## [1] "Antes hab<U+00ED>a espacios en blanco al inicio y al final"

Para agregar espacios(ya sea en blanco o con algún patrón) la función a utilizar es str_pad(), por ejemplo:

str_pad(c("a", "abc", "abcdef"), 10)
## [1] "         a" "       abc" "    abcdef"
  1. Escribe una función que convierta (por ejemplo) un vector c("a", "b", "c") en la cadena a, b y c. Piensa con cuidado sobre lo que debería hacer esta función si recibe un vector de longitud 0, 1 o 2.
fix_words <- function(vector){
  if(length(vector) == 0){
    vector <- ""
  }else{
    sub_string <- str_c(vector[c(1:length(vector)-1)], collapse = ", ")
    vector <- str_c(sub_string, vector[[length(vector)]], sep = " y ")
  }
  return(vector)
}

Ejemplo:

numbers <- c("uno","dos","tres","cuatro","cinco")
numbers <- fix_words(numbers)
numbers
## [1] "uno, dos, tres, cuatro y cinco"
  1. Explica por qué cada una de estas cadenas no coinciden con un \: "\", "\\", "\\\".

“" no funciona ya que hasta este punto, el caracter anterior espera”escapar" algún caracter próximo, de hecho, si se escribe la anterior expresión, R espera que se continue con la escritura:

“" +

“\” no funciona porque lo anterior solo escapa “" pero no crea la expresión regular”\" para encontrar patrones, solo el caracter “"

“\" tiene un resultado similar a la primera expresión, al colocar la tercera”" se espera que continue la escritura ya que al agregarla se esta creando la cadena pero no se ha escapado por lo cual se necesita la cuarta barra invertida

“\" +

  1. ¿Cómo coincidirías la secuencia "'\?

Regex : "'\, cadena : “"'\\”

Ejemplo :

ex <- c("Esta es una palabra \"\'\\ rara", "esta no")
writeLines(ex)
## Esta es una palabra "'\ rara
## esta no
str_view(ex, "\"\'\\\\")
  1. ¿Qué patrones encontrará la expresión regular \..\..\..? ¿Cómo lo representarías como una cadena?

Encontrará aquellas que sean de la siguiente forma: Si \(\alpha\) es cualquier caracter, se hara la coindicencia sobre las expresiones que contengan los siguiente, ya sea como una subcadena o ella misma:

\[.\alpha.\alpha.\alpha\] Por ejemplo : .h.o.l.a, .a.d.i.s, .3.4.5.8

La expresión anterior se representa con la siguiente cadena: “\..\..\..”

str_view("estas pal.a.|.%.as estan chistosas", "\\..\\..\\..")
  1. ¿Cómo coincidirías con la cadena literal "$^$"?

Regex : "$^$" , cadena : “\”\\(\\^\\\)\“”

Ejemplo :

str_view('si encontráramos alguna frase con "$^$" no hay que creérle XD', '\\"\\$\\^\\$\\"')
  1. Dado el corpus de palabras en stringr::words, crea expresiones regulares que encuentren todas las palabras que:

    • Comienzan con “y”.
    • Terminan con “x”
    • Tienen exactamente tres letras de largo. (¡No hagas trampa al usar str_length()!)
    • Tener siete letras o más.

Dado que esta lista es larga, es posible que desees utilizar el argumento match de str_view() para mostrar solo las palabras coincidentes o no coincidentes.

str_view(words, "^y", match = TRUE)
str_view(words, "x$", match = TRUE)
str_view(words, "^...$", match = TRUE)
str_view(words, ".{7,}", match = TRUE)
  1. Crea expresiones regulares para encontrar todas las palabras que:

    • Comienzan con una vocal.
    • Solo contienen consonantes. (Sugerencia: piensa en encontrar “no” -vocales).
    • Terminan con ed, pero no con eed.
    • Terminan con ing o ise.

Comienzan con una vocal

str_view(words, "\\b[AEIOUaeiou]\\S*\\b", match = TRUE)

Otro ejemplo:

str_view_all("Algunas palabras de esta frase no empiezan con vocales", "\\b[AEIOUaeiou]\\S*\\b", match = TRUE)

Solo contienen consonantes

str_view(words, "\\b[^AEIOUaeiou\\s]+\\b", match = TRUE)

Otro ejemplo :

str_view("she sells seashells soles sssss aafsa", "\\b[^AEIOUaeiou\\s]+\\b", match = TRUE)

Terminan con ed, pero no con eed

str_view(words, "\\b([^\\s]*[^e]ed|ed)\\b", match = TRUE)

Otro ejemplo:

str_view_all("ed termina como bed y como red pero edd y eddy no", "\\b([^\\s]*[^e]ed|ed)\\b", match = TRUE)

Terminan con ing o ise

str_view(words, "\\b[^\\s]*(ing|ise)\\b", match = TRUE)

Otro ejemplo:

str_view_all("When I did exercise I was drinking a bottle of water", "\\b[^\\s]*(ing|ise)\\b")
  1. Crea una expresión regular que coincida con los números de teléfono escritos comúnmente en tu país.

Vamos a crear una función que contenga la Lada, ya que existen muchas, se pueden consultar, para México, en la siguiente liga.

LADA_regex <- function(string){
  return(str_c("^",toString(string), "\\w{8}$"))
}
#Para la CDMX
DF <- LADA_regex(55)
#Ejemplo:
numeros <- c("27113073698", "5578452906", "79563697465", "5165984509")
str_view(numeros, DF)
  1. Describe los equivalentes de ?, +, * en la forma {m,n}.

Para ? : {0,1} Para + : {1,} Para * : {0,}

  1. Crea expresiones regulares para encontrar todas las palabras que:

    • Comienzan con tres consonantes.
    • Tienen tres o más vocales seguidas.
    • Tienen dos o más parejas de vocales y consonantes seguidas.
    • Resuelve los crucigramas de la expresión regular para principiantes en https://regexcrossword.com/challenges/beginner.

Comienzan con tres consonantes :

#Seguiremos con "words"
str_view(words, "\\b[^AEIOUaeiou\\s]{3}[^\\s]*", match = TRUE)

Otro ejemplo:

str_view_all("Christmas is coming and fly", "\\b[^AEIOUaeiou\\s]{3}[^\\s]*")

Tienen tres o más vocales seguidas : [aeiou]{3,}

str_view(words, "\\S*[aeiou]{3,}\\S*", match = TRUE)

Otro ejemplo:

str_view_all("You are beautiful and it's obvious", "\\S*[aeiou]{3,}\\S*", match = TRUE)

Tienen dos o más parejas de vocales y consonantes seguidas : ([aeiou]{2,}|[^aeiou]{2,})

str_view_all(words, "\\S*([aeiou]{4,}|[^aeiou\\s]{4,})", match = TRUE)

Otro ejemplo :

str_view_all("While I apply a function the industry was working", "\\S*([aeiou]{4,}|[^aeiou\\s]{4,})", match = TRUE)
  1. Construya expresiones regulares para unir palabras que:

    • Comienzan y terminan con el mismo caracter.
    • Contienen un par de letras repetidas (por ejemplo, “church” contiene “ch” repetido dos veces).
    • Contienen una letra repetida en al menos tres lugares (por ejemplo, “eleven” contiene tres “e”).

Se considera una palabra a una sucesión de caracteres (al menos dos) Comienzan y terminan con el mismo caracter:

Con la expresión regular ([A-z])+se obtienen aquellas palabras que comienzan y terminan con el mismo caracter

#str_view_all(words, "\\b([A-z])[a-z]+\\1\\b", match = TRUE)
str_view_all("she sells seashells soles sssss aafsa", "\\b([A-z])\\S+\\1\\b")

Ahora, para unir las palabras de una frase se hace lo siguiente :

concatenar <- function(palabra, regex){
  coincidencias <- str_extract_all(palabra, regex, simplify = TRUE)
  coincidencias <- coincidencias[1,]
  coincidencias <- coincidencias[!is.na(coincidencias)]
  union <- str_c(coincidencias, collapse = ", ")
  return(union)
}
concatenar("america is an area where the education its essential", "\\b([A-z])[a-z]+\\1\\b")
## [1] "america, area"

Contienen un par de letras repetidas (por ejemplo, “church” contiene “ch” repetido dos veces)

regex: ()[A-z]*

concatenar("como estan por ahihihi alohohoho dsdfsdf banana como estan por holhos banatynap", "\\b[^\\s]*(\\S\\S)[A-z]*\\1[A-z]*\\b")
## [1] "ahihihi, alohohoho, dsdfsdf, banana, holhos, banatynap"

Contienen una letra repetida en al menos tres lugares (por ejemplo, “eleven” contiene tres “e”) Regex : ([A-z])[A-z]

concatenar("hay cosas como el ferrocaril que contienen tres letras como eleven", " \\b[A-z]*([A-z])[A-z]*\\1[A-z]*\\1[A-z]*\\b")
## [1] " ferrocaril,  contienen,  eleven"
  1. Para cada uno de los siguientes desafíos, intenta resolverlo utilizando una sola expresión regular y una combinación de llamadas múltiples a str_detect().

    • Encuentra todas las palabras que comienzan o terminan en x.
    • Encuentra todas las palabras que comienzan con una vocal y terminan con una consonante.
    • ¿Hay alguna palabra que contenga al menos una de cada vocal diferente?

Encuentra todas las palabras que comienzan o terminan en x

new_words <- c("xray","rayos x", "combinar", "xrayosx", ":)", "when", "did", "exercise", "was", "drinking", "bottle", "of", "water", "xD", "XD", "XDX", "XD", "xD", "You", "are", "ferrocarrilu", "beautiful", "and", "it's", "obvious", "The", "xRegex", "is", "the", "better", "it", "isn't", "so", "difficult")
new_words[str_detect(new_words, "\\b(x|X)[^\\s]*(x|X)\\b")]
## [1] "xrayosx" "XDX"     "xRegex"

Encuentra todas las palabras que comienzan con una vocal y terminan con una consonante

new_words[str_detect(new_words, "\\b[AEIOUaeiou]\\S*[^AEIOUaeiou\\s]\\b")]
## [1] "of"      "and"     "it's"    "obvious" "is"      "it"      "isn't"

¿Hay alguna palabra que contenga al menos una de cada vocal diferente?

A <- new_words[str_detect(new_words, "\\S*[A|a]\\S*")]
E <- A[str_detect(A, "\\S*[E|e]\\S*")]
I <- E[str_detect(E, "\\S*[I|i]\\S*")]
O <- I[str_detect(I, "\\S*[O|o]\\S*")]
U <- O[str_detect(O, "\\S*[U|u]\\S*")]
U
## [1] "ferrocarrilu"
  1. ¿Qué palabra tiene el mayor número de vocales? ¿Qué palabra tiene la mayor proporción de vocales? (Sugerencia: ¿cuál es el denominador?)

1:

new_words[str_count(new_words, "[AEIOUaeiou]") == max(str_count(new_words, "[AEIOUaeiou]"))]
## [1] "ferrocarrilu" "beautiful"
words[str_count(words, "[AEIOUaeiou]") == max(str_count(words, "[AEIOUaeiou]"))]
## [1] "appropriate" "associate"   "available"   "colleague"   "encourage"  
## [6] "experience"  "individual"  "television"

2:

words[str_count(words, "[AEIOUaeiou]")/str_count(words) == 
        max(str_count(words, "[AEIOUaeiou]")/str_count(words))]
## [1] "a"
new_words[str_count(new_words, "[AEIOUaeiou]")/str_count(new_words) == 
        max(str_count(new_words, "[AEIOUaeiou]")/str_count(new_words))]
## [1] "You" "are"
  1. Encuentra todas las contracciones. Separa las piezas antes y después del apóstrofe.
contractions <- function(Data){
  result <- tibble(phrases = Data)
  result <- result %>% mutate(
    before = str_extract(phrases, "\\S*(´|')"),
    after = str_extract(phrases, "(´|')\\S*")) %>% 
    filter(!is.na(before)) %>% 
    mutate(before = str_sub(before, 1, -2), 
           after = str_sub(after, 2, -1)) %>% 
    filter(!is.na(before))
  return(result)
}
contractions(sentences)
  1. Reemplaza todas las barras diagonales en una cadena con barras diagonales inversas.
almanaque <- c("la primera vez fue el 8/05/13, después en 12/09/14 y finalmente en 29/11/17", "la última vez fue el 6/04/98, después en 12/09/99 y finalmente en 29/11/04", "la primera vez fue el 7/05/92, después en 12/09/92 y finalmente en 31/11/92")

writeLines(str_replace_all(almanaque, "/", "\\\\"))
## la primera vez fue el 8\05\13, despu<U+00E9>s en 12\09\14 y finalmente en 29\11\17
## la <U+00FA>ltima vez fue el 6\04\98, despu<U+00E9>s en 12\09\99 y finalmente en 29\11\04
## la primera vez fue el 7\05\92, despu<U+00E9>s en 12\09\92 y finalmente en 31\11\92
  1. Implementa una versión simple de str_to_lower() usando replace_all().
to_lower <- function(base){
  base <- str_replace_all(base, c("A" = "a", 
                                  "B" = "b", 
                                  "C" = "c", 
                                  "D" = "d", 
                                  "E" = "e", 
                                  "F" = "f", 
                                  "G" = "g", 
                                  "H" = "h", 
                                  "I" = "i", 
                                  "J" = "j", 
                                  "K" = "k", 
                                  "L" = "l", 
                                  "M" = "m", 
                                  "N" = "n", 
                                  "O" = "o", 
                                  "P" = "p", 
                                  "Q" = "q", 
                                  "R" = "r", 
                                  "S" = "s", 
                                  "T" = "t",
                                  "U" = "u",
                                  "V" = "v", 
                                  "W" = "w", 
                                  "X" = "x", 
                                  "Y" = "y", 
                                  "Z" = "z"))
  return(base)
}

#Ejemplo :
to_lower(c("HOLA ESTA FRASE ESTABA EN MAYUSCULAS"))
## [1] "hola esta frase estaba en mayusculas"
  1. Cambia la primera y la última letra en words. ¿Cuáles de esas cadenas son todavía palabras?

Las intercambiaremos, entonces aquellas que seguiran siendo palabras son las que terminen y empiezen con la misma letra (letra, no caracter necesariamente)

exchange <- function(data){
  beginning <- str_extract(data, "\\b[A-z]")
  final <- str_extract(data, "[A-z]\\b")
  data <- str_replace(data, "\\b[A-z]", final)
  data <- str_replace(data, "[A-z]\\b", beginning)
  return(data)
}
exchange(words) %>% head(18)
##  [1] "a"         "ebla"      "tboua"     "ebsoluta"  "tccepa"   
##  [6] "tccouna"   "echieva"   "scrosa"    "tca"       "ectiva"   
## [11] "lctuaa"    "dda"       "sddresa"   "tdmia"     "edvertisa"
## [16] "tffeca"    "dffora"    "rftea"
  1. Divide una cadena como “apples, pears, and bananas” en componentes individuales.
str_split("apples, pears, and bananas", "(, | )")
## [[1]]
## [1] "apples"  "pears"   "and"     "bananas"
  1. ¿Por qué es mejor dividir con boundary(“word”) que con " “?

El caso anterior es un claro ejemplo, al dividir en palabras “apples, pears, and bananas” con " " no considera la coma o “,” no considera cuando no se tiene la coma y con boundary no

str_split("apples, pears, and bananas", boundary("word"))
## [[1]]
## [1] "apples"  "pears"   "and"     "bananas"
  1. ¿Qué hace la división con una cadena vacía (“”)? Experimenta y luego lee la documentación.
str_split("apples, pears, and bananas", "")
## [[1]]
##  [1] "a" "p" "p" "l" "e" "s" "," " " "p" "e" "a" "r" "s" "," " " "a" "n"
## [18] "d" " " "b" "a" "n" "a" "n" "a" "s"

La documentación dice que “” es un limite por caracter.

  1. ¿Cuáles son las cinco palabras más comunes en sentences?
oraciones <- str_split(sentences, boundary("word"), simplify = TRUE)
oraciones <- as.vector(oraciones)[oraciones != ""] # Así omitimos los caracteres "" originados en la matriz echa
oraciones <- str_to_lower(oraciones) #Normalizamos
tibble(sentences = oraciones) %>% 
  group_by(sentences) %>% 
  dplyr::summarise(count = n()) %>%
  arrange(desc(count)) %>% 
  head(5)
  1. Encuentra las funciones de stringi que:

Cuenta el número de palabras : stri_count_words()

library(stringi)
stri_count_words("esta oracion tiene 5 palabras")
## [1] 5

Encuentra cadenas duplicadas : stri_duplicated()

Hay dos funciones de stri_duplicated() :

stri_duplicated() determina cuales caracteres en un vector de caracteres son duplicados con respecto a los elementos anteriores en el mismo vector.

stri_duplicated_any() determina si hay alguna cantidad de caracteres duplicados en un vector.

stri_duplicated(c("a", "b", "a", NA, "a", NA))
## [1] FALSE FALSE  TRUE FALSE  TRUE  TRUE
stri_duplicated_any(c("a", "b", "a", NA, "a", NA))
## [1] 3

Genera texto aleatorio : stri_rand_strings()

stri_rand_strings(5, 10) 
## [1] "qCDw77qCbz" "ynV8HqYg8T" "QGvz0Z4IvK" "A9Q7gLVkvO" "10y6CfIsRa"

Documentación de Stringi

  1. ¿Cómo controlas el lenguaje que stri_sort() usa para ordenar?

La documentación dice : sorts the vector according to a lexicographic order; por lo que ordena de acuerdo a la ubicación asignada:

 stri_sort(c("hladny", "chladny"), locale="pl_PL")
## [1] "chladny" "hladny"
stri_sort(c("hladny", "chladny"), locale="sk_SK")
## [1] "hladny"  "chladny"

Respuestas CarlosFVG

forcats

  1. Explora la distribución de rincome en forcats::gss_cat (ingreso reportado). ¿Qué hace que la gráfica de barras predeterminada sea difícil de entender? ¿Cómo podrías mejorarla?

Es dificil entender por que el orden de los factores van de mayor a menor, la altura de la barra no importa, si no, el orden del eje inferior.

gss_cat %>% select(rincome) %>% 
  ggplot(aes(rincome))+
  geom_bar()

levels_rincome <- c("Lt $1000", "$1000 to 2999", "$3000 to 3999", "$4000 to 4999" , "$5000 to 5999", "$6000 to 6999", "$7000 to 7999" , "$8000 to 9999", "$10000 - 14999" , "$15000 - 19999", "$20000 - 24999", "$25000 or more" , "No answer" , "Don't know" , "Refused", "Not applicable")
rincome_new_leves <- gss_cat %>% mutate(rincome = factor(rincome, levels = levels_rincome))
rincome_new_leves %>% select(rincome) %>% 
  ggplot(aes(rincome))+
  geom_bar()

  1. ¿Cuál es el más común relig en esta encuesta? ¿Cuál es el más común partyid?
#Tenemos dos maneras de saberlo, en un data frame y de forma gráfica
gss_cat %>% 
  group_by(relig) %>% 
  dplyr::summarise(count = n()) %>% 
  arrange(desc(count)) %>% 
  head(1)
gss_cat %>% 
  mutate(relig = relig %>% fct_infreq() %>% fct_rev()) %>%
  ggplot(aes(relig))+
  geom_bar()

Para partyid se modificarán los niveles:

partyid_new <- gss_cat %>%
  mutate(partyid = fct_recode(partyid,
    "Republican, strong"    = "Strong republican",
    "Republican, weak"      = "Not str republican",
    "Independent, near rep" = "Ind,near rep",
    "Independent, near dem" = "Ind,near dem",
    "Democrat, weak"        = "Not str democrat",
    "Democrat, strong"      = "Strong democrat",
    "Other"                 = "No answer",
    "Other"                 = "Don't know",
    "Other"                 = "Other party"
  )) 

partyid_new %>% 
  group_by(partyid) %>% 
  dplyr::summarise(count = n()) %>% 
  arrange(desc(count)) %>%
  head(1)
partyid_new %>% 
  mutate(partyid = partyid %>% fct_infreq() %>% fct_rev()) %>%
  ggplot(aes(partyid))+
  geom_bar()

  1. Hay algunos números sospechosamente altos en tvhours. ¿Es la media un buen resumen?

Al menos considrando esos números sospechosos, la media no es un buen resumen ya que estos números pueden afectar de manera drástica los resultados.

Los valores sospechosos se representan en el siguiente gráfico

gss_cat %>% filter(!is.na(tvhours)) %>% ggplot(aes(x = "", y = tvhours)) + geom_boxplot() 

  1. Para cada factor en gss_cat identifica si el orden de los niveles es arbitrario o principal.

Los factores en gss_cat son marital, race, rincome, partyid, relig y denom

levels(gss_cat$marital)
## [1] "No answer"     "Never married" "Separated"     "Divorced"     
## [5] "Widowed"       "Married"

El orden es principal.

levels(gss_cat$race)
## [1] "Other"          "Black"          "White"          "Not applicable"

EL orden es arbitrario.

levels(gss_cat$rincome)
##  [1] "No answer"      "Don't know"     "Refused"        "$25000 or more"
##  [5] "$20000 - 24999" "$15000 - 19999" "$10000 - 14999" "$8000 to 9999" 
##  [9] "$7000 to 7999"  "$6000 to 6999"  "$5000 to 5999"  "$4000 to 4999" 
## [13] "$3000 to 3999"  "$1000 to 2999"  "Lt $1000"       "Not applicable"

Es un poco arbitrario el orden por el factor “Not applicable” y va en dorma descendente con respecto a la cantidad.

levels(gss_cat$partyid)
##  [1] "No answer"          "Don't know"         "Other party"       
##  [4] "Strong republican"  "Not str republican" "Ind,near rep"      
##  [7] "Independent"        "Ind,near dem"       "Not str democrat"  
## [10] "Strong democrat"

Es arbitrario.

levels(gss_cat$relig)
##  [1] "No answer"               "Don't know"             
##  [3] "Inter-nondenominational" "Native american"        
##  [5] "Christian"               "Orthodox-christian"     
##  [7] "Moslem/islam"            "Other eastern"          
##  [9] "Hinduism"                "Buddhism"               
## [11] "Other"                   "None"                   
## [13] "Jewish"                  "Catholic"               
## [15] "Protestant"              "Not applicable"

Es arbitrario.

levels(gss_cat$denom)
##  [1] "No answer"            "Don't know"           "No denomination"     
##  [4] "Other"                "Episcopal"            "Presbyterian-dk wh"  
##  [7] "Presbyterian, merged" "Other presbyterian"   "United pres ch in us"
## [10] "Presbyterian c in us" "Lutheran-dk which"    "Evangelical luth"    
## [13] "Other lutheran"       "Wi evan luth synod"   "Lutheran-mo synod"   
## [16] "Luth ch in america"   "Am lutheran"          "Methodist-dk which"  
## [19] "Other methodist"      "United methodist"     "Afr meth ep zion"    
## [22] "Afr meth episcopal"   "Baptist-dk which"     "Other baptists"      
## [25] "Southern baptist"     "Nat bapt conv usa"    "Nat bapt conv of am" 
## [28] "Am bapt ch in usa"    "Am baptist asso"      "Not applicable"

Es arbitrario.

  1. ¿Por qué mover “No aplicable” de rincome al frente de los niveles lo mueve al final de la gráfica?
rincome_summary <- gss_cat %>%
  group_by(rincome) %>%
  summarise(
    age = mean(age, na.rm = TRUE),
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )
ggplot(rincome_summary, 
       aes(age, fct_relevel(rincome, "Not applicable"))) + geom_point()

Por que al referirse “al frente” se considera como en una pila donde el de “en frente” hace referencia al primero, otra cosa a notar es que los niveles estan ordenados repecto al ingreso reportado y entre mayor es este, más abajo estará el nivel.

  1. ¿Cómo han cambiado con el tiempo las proporciones de personas que se identifican como demócratas, republicanos e independientes?
library(plotly)
other_nivels <- gss_cat %>%
  mutate(partyid = fct_collapse(partyid,
    other = c("No answer", "Don't know", "Other party"),
    reprepublican = c("Strong republican", "Not str republican"),
    independent = c("Ind,near rep", "Independent", "Ind,near dem"),
    democrat = c("Not str democrat", "Strong democrat")
  ))

by_year <- other_nivels %>%
  group_by(partyid, year) %>%
  dplyr::summarise(count = n()) %>% 
  group_by(year) %>% 
  mutate(prop = count/sum(count)) %>% arrange(year)

grap <- ggplot(by_year, aes(year, prop, colour = fct_reorder2(partyid, year, prop))) +
  geom_line(na.rm = TRUE)+
  labs(colour = "partyid")+ 
  ggtitle("Proportion of people by political tendency")

ggplotly(grap)
  1. ¿Cómo podrías colapsar rincome en un pequeño conjunto de categorías?
levels(gss_cat$rincome)
##  [1] "No answer"      "Don't know"     "Refused"        "$25000 or more"
##  [5] "$20000 - 24999" "$15000 - 19999" "$10000 - 14999" "$8000 to 9999" 
##  [9] "$7000 to 7999"  "$6000 to 6999"  "$5000 to 5999"  "$4000 to 4999" 
## [13] "$3000 to 3999"  "$1000 to 2999"  "Lt $1000"       "Not applicable"
new_levels <- gss_cat %>%
  mutate(rincome = fct_collapse(rincome,
    other = c("No answer", "Don't know", "Refused", "Not applicable"),
    more_than_10000 = c("$10000 - 14999", "$15000 - 19999", "$20000 - 24999", "$25000 or more"),
    less_than_10000 = c("$5000 to 5999", "$6000 to 6999", "$7000 to 7999", "$8000 to 9999"),
    less_than_5000 = c("Lt $1000", "$1000 to 2999", "$3000 to 3999", "$4000 to 4999")
  ))

levels(new_levels$rincome)
## [1] "other"           "more_than_10000" "less_than_10000" "less_than_5000"

Respuestas CarlosFVG

lubridate

  1. ¿Qué sucede si parseas una cadena que contiene fechas no válidas?

Se genera un error de parseo ya que no reconoce el formato para convertir en formato fecha.

library(lubridate)
ymd(c("2010-10-10", "bananas"))
## Warning: 1 failed to parse.
## [1] "2010-10-10" NA
  1. ¿Qué hace el argumento tzone para today()? ¿Por qué es importante?

Sirve para especificar la zona horaria para encontrar la fecha actual, por ejemplo:

today("GMT")
## [1] "2018-08-02"
today("UTC-09")
## Warning in with_tz(Sys.time(), tzone): Unrecognized time zone 'UTC-09'
## Warning in as.POSIXlt.POSIXct(x, tz = tz): unknown timezone 'UTC-09'
## [1] "2018-08-03"
  1. Use la función de lubridate apropiada para parsear cada una de las siguientes fechas:
d1 <- "January 1, 2010"
d2 <- "2015-Mar-07"
d3 <- "06-Jun-2017"
d4 <- c("August 19 (2015)", "July 1 (2015)")
d5 <- "12/30/14" # 30 Dic, 2014

mdy(d1)
## [1] "2010-01-01"
ymd(d2)
## [1] "2015-03-07"
dmy(d3)
## [1] "2017-06-06"
mdy(d4)
## [1] "2015-08-19" "2015-07-01"
mdy(d5)
## [1] "2014-12-30"
  1. ¿Cómo cambia la distribución de los tiempos de vuelo en un día a lo largo del año?
library(nycflights13)
make_datetime_100 <- function(year, month, day, time) {
  make_datetime(year, month, day, time %/% 100, time %% 100)
}

flights_dt <- flights %>% 
  filter(!is.na(dep_time), !is.na(arr_time)) %>% 
  mutate(
    dep_time = make_datetime_100(year, month, day, dep_time),
    arr_time = make_datetime_100(year, month, day, arr_time),
    sched_dep_time = make_datetime_100(year, month, day, sched_dep_time),
    sched_arr_time = make_datetime_100(year, month, day, sched_arr_time), 
    air_time = make_datetime_100(year, month, day, air_time)
  ) %>% 
  select(origin, dest, ends_with("delay"), ends_with("time"))

flights_dt %>% 
  filter(air_time < ymd(20130102)) %>% 
  ggplot(aes(air_time)) + 
  geom_freqpoly(binwidth = 600)

  1. Escribe una función que, dada tu fecha de cumpleaños (como fecha), devuelva la edad que tienes en años.
years_old <- function(fecha){
  years <- today() - ymd(fecha)
  years <- as.duration(years)
  return(years)
}

my_birthday <- "19960808"
years_old(my_birthday)
## [1] "693705600s (~21.98 years)"